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
8 changes: 8 additions & 0 deletions includes/create-theme/resolver_additions.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

require_once __DIR__ . '/theme-templates.php';

function cbt_augment_resolver_with_utilities() {

//Ultimately it is desireable for Core to have this functionality natively.
Expand Down Expand Up @@ -79,6 +81,12 @@ public static function export_theme_data( $content, $extra_theme_data = null ) {

$data = $theme->get_data();

if ( class_exists( 'CBT_Theme_Templates' ) ) {
$parts_export_type = ( 'variation' === $content ) ? 'user' : $content;
$exported_parts = CBT_Theme_Templates::get_theme_templates( $parts_export_type )->parts;
$data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $data, $exported_parts );
}

// Add the schema.
if ( empty( $schema ) ) {
global $wp_version;
Expand Down
87 changes: 87 additions & 0 deletions includes/create-theme/theme-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,93 @@ public static function add_templates_to_local( $export_type, $path = null, $slug
);
}
}

if ( ! empty( $theme_templates->parts ) ) {
self::update_theme_json_template_parts( $base_dir, $theme_templates->parts );
}
}

/**
* Ensure each exported template part has a templateParts entry with name, title, and area
* in theme.json, unless an entry for that name already exists.
*
* @param array $theme_json_data theme.json data array.
* @param array $parts Array of WP_Block_Template objects.
* @return array Updated theme.json data array.
*/
public static function add_template_parts_to_theme_json_data( array $theme_json_data, array $parts ) {
if ( empty( $parts ) ) {
return $theme_json_data;
}

$existing_by_name = array();
if ( isset( $theme_json_data['templateParts'] ) && is_array( $theme_json_data['templateParts'] ) ) {
foreach ( $theme_json_data['templateParts'] as $index => $part_entry ) {
if ( is_array( $part_entry ) && isset( $part_entry['name'] ) ) {
$existing_by_name[ $part_entry['name'] ] = $index;
}
}
} else {
$theme_json_data['templateParts'] = array();
}

foreach ( $parts as $part ) {
if ( ! is_object( $part ) || empty( $part->slug ) ) {
continue;
}

$slug = sanitize_key( $part->slug );
$title = ! empty( $part->title ) ? sanitize_text_field( $part->title ) : $slug;
$area = ! empty( $part->area ) ? sanitize_key( $part->area ) : 'uncategorized';

if ( isset( $existing_by_name[ $slug ] ) ) {
$idx = $existing_by_name[ $slug ];
if ( empty( $theme_json_data['templateParts'][ $idx ]['area'] ) && ! empty( $area ) ) {
$theme_json_data['templateParts'][ $idx ]['area'] = $area;
}
if ( empty( $theme_json_data['templateParts'][ $idx ]['title'] ) && ! empty( $title ) ) {
$theme_json_data['templateParts'][ $idx ]['title'] = $title;
}
continue;
}

$theme_json_data['templateParts'][] = array(
'area' => $area,
'name' => $slug,
'title' => $title,
);
$existing_by_name[ $slug ] = count( $theme_json_data['templateParts'] ) - 1;
}

return $theme_json_data;
}

/**
* Update theme.json in the specified base directory to include entries for template parts.
*
* @param string $base_dir Base directory of the theme.
* @param array $parts Array of template part objects.
*/
public static function update_theme_json_template_parts( $base_dir, array $parts ) {
$theme_json_path = $base_dir . DIRECTORY_SEPARATOR . 'theme.json';
if ( ! file_exists( $theme_json_path ) ) {
return;
}

$theme_json_raw = file_get_contents( $theme_json_path );
$theme_json = json_decode( $theme_json_raw, true );
if ( ! is_array( $theme_json ) ) {
return;
}

$updated_theme_json = self::add_template_parts_to_theme_json_data( $theme_json, $parts );
if ( $updated_theme_json !== $theme_json ) {
file_put_contents( $theme_json_path, CBT_Theme_JSON_Resolver::stringify( $updated_theme_json ) );
if ( class_exists( 'CBT_Theme_JSON_Resolver' ) ) {
CBT_Theme_JSON_Resolver::clean_cached_data();
}
wp_get_theme()->cache_delete();
}
}

/**
Expand Down
134 changes: 134 additions & 0 deletions tests/test-theme-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,138 @@ public function test_localize_cover_repeated_background_inline_style() {
$new_template = CBT_Theme_Media::make_template_images_local( $template );
$this->assertStringContainsString( '<?php echo esc_url( get_template_directory_uri() ); ?>/assets/images/pattern.webp', $new_template->content );
}

/**
* Ensure that add_template_parts_to_theme_json_data adds new template parts to theme.json data.
*/
public function test_add_template_parts_to_theme_json_data_adds_new_parts() {
$part = new stdClass();
$part->slug = 'custom-header';
$part->title = 'Custom Header';
$part->area = 'header';

$input_data = array(
'version' => 3,
);
$output_data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $input_data, array( $part ) );

$this->assertArrayHasKey( 'templateParts', $output_data );
$this->assertCount( 1, $output_data['templateParts'] );
$this->assertSame( 'custom-header', $output_data['templateParts'][0]['name'] );
$this->assertSame( 'Custom Header', $output_data['templateParts'][0]['title'] );
$this->assertSame( 'header', $output_data['templateParts'][0]['area'] );
}

/**
* Ensure that add_template_parts_to_theme_json_data does not overwrite or duplicate existing template parts.
*/
public function test_add_template_parts_to_theme_json_data_does_not_duplicate_existing() {
$part = new stdClass();
$part->slug = 'header';
$part->title = 'New Header Title';
$part->area = 'header';

$input_data = array(
'version' => 3,
'templateParts' => array(
array(
'area' => 'header',
'name' => 'header',
'title' => 'Original Header',
),
),
);
$output_data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $input_data, array( $part ) );

$this->assertCount( 1, $output_data['templateParts'] );
$this->assertSame( 'Original Header', $output_data['templateParts'][0]['title'] );
}

/**
* Ensure that add_template_parts_to_theme_json_data falls back to uncategorized and slug title when missing.
*/
public function test_add_template_parts_to_theme_json_data_defaults_area_and_title() {
$part = new stdClass();
$part->slug = 'custom-sidebar';

$input_data = array( 'version' => 3 );
$output_data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $input_data, array( $part ) );

$this->assertCount( 1, $output_data['templateParts'] );
$this->assertSame( 'custom-sidebar', $output_data['templateParts'][0]['name'] );
$this->assertSame( 'custom-sidebar', $output_data['templateParts'][0]['title'] );
$this->assertSame( 'uncategorized', $output_data['templateParts'][0]['area'] );
}

/**
* Ensure that update_theme_json_template_parts persists template parts directly into theme.json on disk.
*/
public function test_update_theme_json_template_parts_updates_file() {
$temp_dir = get_temp_dir() . 'cbt-test-theme-' . uniqid();
wp_mkdir_p( $temp_dir );

$initial_json = array(
'version' => 3,
'templateParts' => array(
array(
'area' => 'header',
'name' => 'header',
'title' => 'Header',
),
),
);
file_put_contents( $temp_dir . '/theme.json', wp_json_encode( $initial_json ) );

$part = new stdClass();
$part->slug = 'custom-footer';
$part->title = 'Custom Footer';
$part->area = 'footer';

CBT_Theme_Templates::update_theme_json_template_parts( $temp_dir, array( $part ) );

$saved_content = json_decode( file_get_contents( $temp_dir . '/theme.json' ), true );

$this->assertCount( 2, $saved_content['templateParts'] );
$this->assertSame( 'header', $saved_content['templateParts'][0]['name'] );
$this->assertSame( 'custom-footer', $saved_content['templateParts'][1]['name'] );
$this->assertSame( 'footer', $saved_content['templateParts'][1]['area'] );

// Clean up.
unlink( $temp_dir . '/theme.json' );
rmdir( $temp_dir );
}

/**
* Ensure that export_theme_data includes user-created template parts under templateParts in theme.json.
*/
public function test_export_theme_data_includes_template_parts() {
if ( ! class_exists( 'CBT_Theme_JSON_Resolver' ) ) {
$this->markTestSkipped( 'CBT_Theme_JSON_Resolver is not loaded.' );
}

$post_id = wp_insert_post(
array(
'post_type' => 'wp_template_part',
'post_name' => 'my-custom-header',
'post_title' => 'My Custom Header',
'post_status' => 'publish',
'post_content' => '<!-- wp:paragraph --><p>Content</p><!-- /wp:paragraph -->',
)
);
wp_set_post_terms( $post_id, get_stylesheet(), 'wp_theme' );
wp_set_post_terms( $post_id, 'header', 'wp_template_part_area' );

try {
$json_str = CBT_Theme_JSON_Resolver::export_theme_data( 'all' );
$data = json_decode( $json_str, true );

$this->assertIsArray( $data );
$this->assertArrayHasKey( 'templateParts', $data );
$names = array_column( $data['templateParts'], 'name' );
$this->assertContains( 'my-custom-header', $names );
} finally {
wp_delete_post( $post_id, true );
CBT_Theme_JSON_Resolver::clean_cached_data();
}
}
}
Loading