Refactor Theme_Readme (readme.txt) PHP class (#626)

* refactor ThemeReadme class

* update phpunit tests config to make it more similar to core

* Add tests for ThemeReadme class public methods

* simplify copyright text creation

* Fix cloned theme reference to original theme

* updates comment

* Theme_Reade::update receives a parameter with the readme content.

* remove not so useful write method

* Moved readme data fetching logic from utils to readme class and fleshed it out to get all sections.

* Fetch and use the readme data in the metadata panel (for recomended plugins)

---------

Co-authored-by: Jason Crist <jcrist@pbking.com>
This commit is contained in:
Matias Benedetto 2024-05-13 10:47:39 -03:00 committed by GitHub
parent 6273f0228c
commit 7aed6ce339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 710 additions and 233 deletions

View file

@ -149,7 +149,7 @@ class Create_Block_Theme_Admin {
// Add readme.txt.
$zip->addFromStringToTheme(
'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Augment style.css
@ -198,7 +198,6 @@ class Create_Block_Theme_Admin {
$theme['recommended_plugins'] = sanitize_textarea_field( $theme['recommended_plugins'] );
$theme['slug'] = $theme_slug;
$theme['template'] = '';
$theme['original_theme'] = wp_get_theme()->get( 'Name' );
$theme['text_domain'] = $theme_slug;

// Use previous theme's tags if custom tags are empty.
@ -218,7 +217,7 @@ class Create_Block_Theme_Admin {
// Add readme.txt.
$zip->addFromStringToTheme(
'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Augment style.css
@ -267,7 +266,7 @@ class Create_Block_Theme_Admin {
$theme['tags_custom'] = sanitize_text_field( $theme['tags_custom'] );
$theme['image_credits'] = sanitize_textarea_field( $theme['image_credits'] );
$theme['recommended_plugins'] = sanitize_textarea_field( $theme['recommended_plugins'] );
$theme['is_parent_theme'] = true;
$theme['is_child_theme'] = true;
$theme['text_domain'] = $child_theme_slug;
$theme['template'] = $parent_theme_slug;
$theme['slug'] = $child_theme_slug;
@ -282,7 +281,7 @@ class Create_Block_Theme_Admin {
// Add readme.txt.
$zip->addFromStringToTheme(
'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Add style.css.
@ -357,7 +356,7 @@ class Create_Block_Theme_Admin {
// Add readme.txt.
file_put_contents(
$blank_theme_path . DIRECTORY_SEPARATOR . 'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Add new metadata.

View file

@ -7,9 +7,10 @@ class Theme_Create {
);

public static function clone_current_theme( $theme ) {

$theme['version'] = '1.0';
$theme['tags_custom'] = implode( ', ', wp_get_theme()->get( 'Tags' ) );
// Default values for cloned themes
$theme['is_cloned_theme'] = true;
$theme['version'] = '1.0';
$theme['tags_custom'] = implode( ', ', wp_get_theme()->get( 'Tags' ) );

// Create theme directory.
$new_theme_path = get_theme_root() . DIRECTORY_SEPARATOR . $theme['slug'];
@ -33,7 +34,11 @@ class Theme_Create {
Theme_Utils::clone_theme_to_folder( $new_theme_path, $theme['slug'], $theme['name'] );
Theme_Templates::add_templates_to_local( 'all', $new_theme_path, $theme['slug'], $template_options );
file_put_contents( path_join( $new_theme_path, 'theme.json' ), MY_Theme_JSON_Resolver::export_theme_data( 'all' ) );
file_put_contents( path_join( $new_theme_path, 'readme.txt' ), Theme_Readme::build_readme_txt( $theme ) );

// Create the text of readme.txt file and write it to the file.
$readme_content = Theme_Readme::create( $theme );
file_put_contents( path_join( $new_theme_path, 'readme.txt' ), $readme_content );

file_put_contents( path_join( $new_theme_path, 'style.css' ), Theme_Styles::update_style_css( file_get_contents( path_join( $new_theme_path, 'style.css' ) ), $theme ) );

if ( $theme['subfolder'] ) {
@ -61,7 +66,7 @@ class Theme_Create {
// Add readme.txt.
file_put_contents(
$new_theme_path . DIRECTORY_SEPARATOR . 'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Add style.css.
@ -109,7 +114,7 @@ class Theme_Create {
// Add readme.txt.
file_put_contents(
$blank_theme_path . DIRECTORY_SEPARATOR . 'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Add new metadata.

View file

@ -1,132 +1,145 @@
<?php

class Theme_Readme {

/**
* Build a readme.txt file for CHILD/GRANDCHILD themes.
* Get the path to the readme.txt file.
*
* @return string
*/
public static function file_path() {
return path_join( get_stylesheet_directory(), 'readme.txt' );
}

/**
* Get the content of the readme.txt file.
*
* @return string
*/
public static function get_content() {
$path = self::file_path();
if ( ! file_exists( $path ) ) {
return '';
}
return file_get_contents( $path );
}

/**
* Creates readme.txt text content from theme data.
*
* @param array $theme The theme data.
* {
* @type string $name The theme name.
* @type string $description The theme description.
* @type string $uri The theme URI.
* @type string $author The theme author.
* @type string $author_uri The theme author URI.
* @type string $copyright_year The copyright year.
* @type string $image_credits The image credits.
* @type string $recommended_plugins The recommended plugins.
* @type bool $is_child_theme Whether the theme is a child theme.
* }
*
* @return string The readme content.
*/
public static function build_readme_txt( $theme ) {
$slug = $theme['slug'];
$name = $theme['name'];
$description = $theme['description'];
$uri = $theme['uri'];
$author = $theme['author'];
$author_uri = $theme['author_uri'];
$copy_year = gmdate( 'Y' );
$wp_version = get_bloginfo( 'version' );
$image_credits = $theme['image_credits'] ?? '';
$recommended_plugins = $theme['recommended_plugins'] ?? '';
$is_parent_theme = $theme['is_parent_theme'] ?? false;
$original_theme = $theme['original_theme'] ?? '';
public static function create( $theme ) {
$name = $theme['name'];
$description = $theme['description'] ?? '';
$uri = $theme['uri'] ?? '';
$author = $theme['author'] ?? '';
$author_uri = $theme['author_uri'] ?? '';
$copy_year = $theme['copyright_year'] ?? gmdate( 'Y' );
$wp_version = $theme['wp_version'] ?? get_bloginfo( 'version' );
$required_php_version = $theme['required_php_version'] ?? '5.7';
$license = $theme['license'] ?? 'GPLv2 or later';
$license_uri = $theme['license_uri'] ?? 'http://www.gnu.org/licenses/gpl-2.0.html';
$image_credits = $theme['image_credits'] ?? '';
$recommended_plugins = $theme['recommended_plugins'] ?? '';
$is_child_theme = $theme['is_child_theme'] ?? false;

// Handle copyright section.
$new_copyright_section = $is_parent_theme || $original_theme ? true : false;
$original_theme_credits = $new_copyright_section ? self::original_theme_credits( $name, $is_parent_theme ) : '';
$copyright_section = self::copyright_section( $new_copyright_section, $original_theme_credits, $name, $copy_year, $author, $image_credits );
// Generates the copyright section text.
$copyright_section_content = self::get_copyright_text( $theme );

// Handle recommended plugins section.
$recommended_plugins_section = self::recommended_plugins_section( $recommended_plugins ) ?? '';
// Create empty readme content
$readme_content = '';

return "=== {$name} ===
// Adds the Theme section.
$theme_section_content = "
Contributors: {$author}
Requires at least: 6.0
Tested up to: {$wp_version}
Requires PHP: 5.7
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

== Description ==

{$description}

== Changelog ==

= 1.0 =
* Initial release
{$recommended_plugins_section}
{$copyright_section}
Requires PHP: {$required_php_version}
License: {$license}
License URI: {$license_uri}
";
$readme_content = self::add_or_update_section( $name, $theme_section_content, $readme_content );

// Adds the Decription section
$readme_content = self::add_or_update_section( 'Description', $description, $readme_content );

// Adds the Changelog section
$initial_changelog = '
= 1.0.0 =
* Initial release
';
$readme_content = self::add_or_update_section( 'Changelog', $initial_changelog, $readme_content );

// Adds the recommended plugins section
$readme_content = self::add_or_update_section( 'Recommended Plugins', $recommended_plugins, $readme_content );

// Adds the Copyright section
$readme_content = self::add_or_update_section( 'Copyright', $copyright_section_content, $readme_content );

// Adds the Images section
$readme_content = self::add_or_update_section( 'Images', $image_credits, $readme_content );

return $readme_content;
}

/**
* Build string for original theme credits.
* Used in readme.txt of cloned themes.
* Get the theme data from the installed theme.
*
* @param string $new_name New theme name.
* @return string
* @return array The theme data.
* {
* @type string $name The theme name.
* @type string $uri The theme URI.
* @type string $author The theme author.
* @type string $license The theme license.
* @type string $license_uri The theme license URI.
* }
*/
static function original_theme_credits( $new_name, $is_parent_theme = false ) {
if ( ! $new_name ) {
return;
}

private static function get_active_theme_data() {
$original_name = wp_get_theme()->get( 'Name' ) ?? '';
$original_uri = wp_get_theme()->get( 'ThemeURI' ) ?? '';
$original_author = wp_get_theme()->get( 'Author' ) ?? '';
$original_readme = get_stylesheet_directory() . '/readme.txt' ?? '';
$original_license = '';
$original_license_uri = '';
$readme_content = file_exists( $original_readme ) ? file_get_contents( $original_readme ) : '';
$original_license = self::get_prop( 'License' );
$original_license_uri = self::get_prop( 'License URI' );

if ( ! $readme_content ) {
return;
}

// Get license from original theme readme.txt
if ( str_contains( $readme_content, 'License:' ) ) {
$starts = strpos( $readme_content, 'License:' ) + strlen( 'License:' );
$ends = strpos( $readme_content, 'License URI:', $starts );
$original_license = trim( substr( $readme_content, $starts, $ends - $starts ) );
}

// Get license URI from original theme readme.txt
if ( str_contains( $readme_content, 'License URI:' ) ) {
$starts = strpos( $readme_content, 'License URI:' ) + strlen( 'License URI:' );
$ends = strpos( $readme_content, '== Description ==', $starts );
$original_license_uri = trim( substr( $readme_content, $starts, $ends - $starts ) );
}

if ( empty( $original_license ) || empty( $original_license_uri ) ) {
return;
}

$theme_credit_content = sprintf(
/* translators: 1: New Theme name, 2: Original Theme Name. 3. Original Theme URI. 4. Original Theme Author. 5. Original Theme License. 6. Original Theme License URI. */
__( '%1$s is based on %2$s (%3$s), (C) %4$s, [%5$s](%6$s)', 'create-block-theme' ),
$new_name,
$original_name,
$original_uri,
$original_author,
$original_license,
$original_license_uri
return array(
'name' => $original_name,
'uri' => $original_uri,
'author' => $original_author,
'license' => $original_license,
'license_uri' => $original_license_uri,
);

if ( $is_parent_theme ) {
$theme_credit_content = sprintf(
/* translators: 1: New Theme name, 2: Parent Theme Name. 3. Parent Theme URI. 4. Parent Theme Author. 5. Parent Theme License. 6. Parent Theme License URI. */
__( '%1$s is a child theme of %2$s (%3$s), (C) %4$s, [%5$s](%6$s)', 'create-block-theme' ),
$new_name,
$original_name,
$original_uri,
$original_author,
$original_license,
$original_license_uri
);
}

return $theme_credit_content;
}

/**
* Build copyright section.
* Used in readme.txt of cloned themes or child themes.
* Build default copyright text for a theme.
*
* @return string
* @param string $name The theme name.
* @param string $copy_year The current year.
* @param string $author The theme author.
* @return string The default copyright text.
*/
static function copyright_section( $new_copyright_section, $original_theme_credits, $name, $copy_year, $author, $image_credits ) {
// Default copyright section.
$copyright_section = "== Copyright ==
private static function get_copyright_text( $theme ) {
$name = $theme['name'];
$year = $theme['copy_year'] ?? gmdate( 'Y' );
$author = $theme['author'] ?? '';

{$name} WordPress Theme, (C) {$copy_year} {$author}
$text = "
{$name} WordPress Theme, (C) {$year} {$author}
{$name} is distributed under the terms of the GNU GPL.

This program is free software: you can redistribute it and/or modify
@ -137,123 +150,184 @@ the Free Software Foundation, either version 2 of the License, or
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.";
GNU General Public License for more details.
";

// If a new copyright section is required, then build ones based on the current theme.
if ( $new_copyright_section ) {
$copyright_section_intro = '== Copyright ==';
$is_child_theme = $theme['is_child_theme'] ?? false;
$is_cloned_theme = $theme['is_cloned_theme'] ?? false;

// Get current theme readme.txt
$current_readme = get_stylesheet_directory() . '/readme.txt' ?? '';
$current_readme_content = file_exists( $current_readme ) ? file_get_contents( $current_readme ) : '';
/*
* If the theme is a child theme or a cloned theme, add a reference to the parent theme.
*
* Example: "My Child Theme is a child theme of My Parent Theme (https://example.org/themes/my-parent-theme), (C) the WordPress team, [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)"
*/
if ( $is_child_theme || $is_cloned_theme ) {
$original_theme = self::get_active_theme_data();

if ( ! $current_readme_content ) {
return;
}
$reference_string = $is_child_theme
? '%1$s is a child theme of %2$s (%3$s), (C) %4$s, [%5$s](%6$s)'
: '%1$s is based on %2$s (%3$s), (C) %4$s, [%5$s](%6$s)';

// Copy copyright section from current theme readme.txt
if ( str_contains( $current_readme_content, $copyright_section_intro ) ) {
$copyright_section_start = strpos( $current_readme_content, $copyright_section_intro );
$copyright_section = substr( $current_readme_content, $copyright_section_start );
$reference = sprintf(
$reference_string,
$name,
$original_theme['name'],
$original_theme['uri'],
$original_theme['author'],
$original_theme['license'],
$original_theme['license_uri']
);

if ( $original_theme_credits ) {
$new_copyright_section = str_replace( $copyright_section_intro . "\n", '', $copyright_section );
$copyright_section = $copyright_section_intro . "\n\n" . $original_theme_credits . "\n" . $new_copyright_section;
}
}
$text .= "\n\n" . $reference;
}

if ( $image_credits ) {
$copyright_section = $copyright_section . "\n" . $image_credits;
}

return $copyright_section;
}

/**
* Build Recommended Plugins section.
*
* @return string
*/
static function recommended_plugins_section( $recommended_plugins, $updated_readme = '' ) {
$recommended_plugins_section = '';

if ( ! $recommended_plugins ) {
return '';
}

$section_start = "\n== Recommended Plugins ==\n";

// Remove existing Recommended Plugins section.
if ( $updated_readme && str_contains( $updated_readme, $section_start ) ) {
$pattern = '/\s+== Recommended Plugins ==\s+(.*?)(?=(\n\=\=)|$)/s';
preg_match_all( $pattern, $updated_readme, $matches );
$current_section = $matches[0][0];
$updated_readme = str_replace( $current_section, '', $updated_readme );
}

$recommended_plugins_section = $section_start . "\n" . $recommended_plugins . "\n";

if ( $updated_readme ) {
return $updated_readme . $recommended_plugins_section;
}

return $recommended_plugins_section;
return $text;
}

/**
* Update current readme.txt file, rather than building a new one.
*
* @param array $theme The theme data.
* {
* @type string $description The theme description.
* @type string $author The theme author.
* @type string $image_credits The image credits.
* @type string $recommended_plugins The recommended plugins.
* }
* @param string $readme_content readme.txt content.
* @return string
*/
public static function update_readme_txt( $theme ) {
$description = $theme['description'];
$author = $theme['author'];
$wp_version = get_bloginfo( 'version' );
public static function update( $theme, $readme_content = '' ) {
// Theme data.
$description = $theme['description'] ?? '';
$author = $theme['author'] ?? '';
$wp_version = $theme['wp_version'] ?? get_bloginfo( 'version' );
$image_credits = $theme['image_credits'] ?? '';
$recommended_plugins = $theme['recommended_plugins'] ?? '';
$updated_readme = '';
$current_readme = get_stylesheet_directory() . '/readme.txt' ?? '';
$readme_content = file_exists( $current_readme ) ? file_get_contents( $current_readme ) : '';

if ( ! $readme_content ) {
return;
}

$updated_readme = $readme_content;

// Update description.
if ( $description ) {
$pattern = '/(== Description ==)(.*?)(\n\n=|$)/s';
preg_match_all( $pattern, $updated_readme, $matches );
$current_description = $matches[0][0];
$updated_readme = str_replace( $current_description, "== Description ==\n\n{$description}\n\n=", $updated_readme );
}
$readme_content = self::add_or_update_section( 'Description', $description, $readme_content );

// Update Author/Contributors.
if ( $author ) {
$pattern = '/(Contributors:)(.*?)(\n|$)/s';
preg_match_all( $pattern, $updated_readme, $matches );
$current_uri = $matches[0][0];
$updated_readme = str_replace( $current_uri, "Contributors: {$author}\n", $updated_readme );
}
$readme_content = self::add_or_update_prop( 'Contributors', $author, $readme_content );

// Update "Tested up to" version.
if ( $wp_version ) {
$pattern = '/(Tested up to:)(.*?)(\n|$)/s';
preg_match_all( $pattern, $updated_readme, $matches );
$current_uri = $matches[0][0];
$updated_readme = str_replace( $current_uri, "Tested up to: {$wp_version}\n", $updated_readme );
$readme_content = self::add_or_update_prop( 'Tested up to', $wp_version, $readme_content );

// Update recommended plugins section.
$readme_content = self::add_or_update_section( 'Recommended Plugins', $recommended_plugins, $readme_content );

// Update image credits section.
$readme_content = self::add_or_update_section( 'Images', $image_credits, $readme_content );

return $readme_content;
}

/**
* Write a section to the readme.txt file.
*
* @param string $section_title Section to write.
* @param string $section_content New content to write.
* @param string $current_content Current content to manipulate.
*
* @return void
*/
public static function add_or_update_section( $section_title, $section_content, $readme_content = '' ) {
// If the section content is empty, return the current content. This avoids adding empty sections.
if ( empty( $section_content ) ) {
return $readme_content;
}

if ( $recommended_plugins ) {
$updated_readme = self::recommended_plugins_section( $recommended_plugins, $updated_readme );
$section_content = trim( $section_content, "\r" );
$section_content = trim( $section_content, "\n" );

// Regular expression to find the section, handling both '==' and '==='
$pattern = '/(={2,3}\s*' . preg_quote( $section_title, '/' ) . '\s*={2,3})(.*?)(?=(={2,3}|$))/s';
$replacement = "== $section_title ==\n\n$section_content\n\n";

// Check if the section exists
if ( preg_match( $pattern, $readme_content ) ) {
// Replace the existing section content
$updated_content = preg_replace( $pattern, $replacement, $readme_content );
} else {
// Remove any trailing whitespace, newlines or carriage returns from current content
$readme_content = rtrim( $readme_content );

// Ensure two newlines before appending new section
if ( ! empty( $readme_content ) ) {
$readme_content .= "\n\n\n";
}

// Append new section if not found
$updated_content = $readme_content . $replacement;
}

if ( $image_credits ) {
$updated_readme = $updated_readme . "\n\n" . $image_credits;
}
return $updated_content;
}

/**
* Adds or updates a property in the readme content.
*
* @param string $prop_name The name of the property.
* @param string $prop_value The value of the property.
* @param string $readme_content The content of the readme file.
* @return string The updated readme content.
*/
private static function add_or_update_prop( $prop_name, $prop_value, $readme_content ) {
if ( empty( $prop_value ) ) {
return $readme_content;
}
$pattern = '/(' . preg_quote( $prop_name, '/' ) . ')(.*?)(\n|$)/s';
preg_match_all( $pattern, $readme_content, $matches );
$current_uri = $matches[0][0];
$updated_readme = str_replace( $current_uri, "{$prop_name}: {$prop_value}\n", $readme_content );
return $updated_readme;
}

/**
* Get property value from the readme content.
*
* @return string The property value
*/
private static function get_prop( $property, $readme_content = '' ) {
if ( empty( $readme_content ) ) {
$readme_content = self::get_content();
}

// Build the regular expression pattern to match the line
$pattern = '/^' . preg_quote( $property, '/' ) . ': (.*)$/m';

// Use preg_match to find a matching line
if ( preg_match( $pattern, $readme_content, $matches ) ) {
// Return the capturing group which contains the value after the colon
return trim( $matches[1] );
} else {
// Return null if no match is found
return null;
}
}

public static function get_sections() {

$readme_content = self::get_content();
$sections = array();

// Regular expression to find the section, handling both '==' and '==='
$pattern = '/(={2,3}\s*(.*?)\s*={2,3})(.*?)(?=(={2,3}|$))/s';

// Find all sections
preg_match_all( $pattern, $readme_content, $matches, PREG_SET_ORDER );

// Loop through the matches
foreach ( $matches as $match ) {
$section_title = str_replace( '-', '_', sanitize_title( $match[2] ) );
$section_content = trim( $match[3] );

// Add the section to the sections array
$sections[ $section_title ] = $section_content;
}

return $sections;
}

}

View file

@ -91,26 +91,6 @@ class Theme_Utils {
}
}

public static function get_readme_data() {
$readme_location = get_template_directory() . '/readme.txt';

if ( ! file_exists( $readme_location ) ) {
throw new Exception( 'No readme file found' );
}

$readme_file_contents = file_get_contents( $readme_location );

$readme_file_details = array();

// Handle Recommended Plugins.
$pattern = '/== Recommended Plugins ==\s+(.*?)(\s+==|$)/s';
preg_match_all( $pattern, $readme_file_contents, $matches );
$readme_file_details['recommendedPlugins'] = $matches[1][0] ?? '';

return $readme_file_details;
}


/**
* Relocate the theme to a new folder and activate the newly relocated theme.
*/

View file

@ -165,7 +165,8 @@ class Create_Block_Theme_API {

function rest_get_readme_data( $request ) {
try {
$readme_data = Theme_Utils::get_readme_data();
$readme_data = Theme_Readme::get_sections();

return new WP_REST_Response(
array(
'status' => 'SUCCESS',
@ -201,7 +202,8 @@ class Create_Block_Theme_API {

function rest_create_child_theme( $request ) {

$theme = $this->sanitize_theme_data( $request->get_params() );
$theme = $this->sanitize_theme_data( $request->get_params() );
$theme['is_child_theme'] = true;
//TODO: Handle screenshots
$screenshot = null;

@ -281,7 +283,7 @@ class Create_Block_Theme_API {
// Add readme.txt.
$zip->addFromStringToTheme(
'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Build style.css with new theme metadata
@ -329,7 +331,7 @@ class Create_Block_Theme_API {
// Add readme.txt.
$zip->addFromStringToTheme(
'readme.txt',
Theme_Readme::build_readme_txt( $theme )
Theme_Readme::create( $theme )
);

// Build style.css with new theme metadata
@ -407,16 +409,16 @@ class Create_Block_Theme_API {
* Update the theme metadata and relocate the theme.
*/
function rest_update_theme( $request ) {
$theme = $request->get_params();
$theme = $this->sanitize_theme_data( $request->get_params() );

// Update the metadata of the theme in the style.css file
$style_css = file_get_contents( get_stylesheet_directory() . '/style.css' );
$style_css = Theme_Styles::update_style_css( $style_css, $theme );
file_put_contents( get_stylesheet_directory() . '/style.css', $style_css );
file_put_contents(
get_stylesheet_directory() . '/readme.txt',
Theme_Readme::update_readme_txt( $theme )
);

$readme_content = Theme_Readme::get_content();
$readme_content = Theme_Readme::update( $theme, $readme_content );
file_put_contents( Theme_Readme::file_path(), $readme_content );

// Replace Screenshot
if ( wp_get_theme()->get_screenshot() !== $theme['screenshot'] ) {

View file

@ -10,7 +10,7 @@
>
<testsuites>
<testsuite name="testing">
<directory prefix="test-" suffix=".php">./tests/</directory>
<directory suffix=".php">./tests/</directory>
<exclude>./tests/test-sample.php</exclude>
</testsuite>
</testsuites>

View file

@ -26,7 +26,7 @@ import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { postUpdateThemeMetadata } from '../resolvers';
import { postUpdateThemeMetadata, fetchReadmeData } from '../resolvers';

const ALLOWED_SCREENSHOT_MEDIA_TYPES = [
'image/png',
@ -53,6 +53,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {

useSelect( async ( select ) => {
const themeData = select( 'core' ).getCurrentTheme();
const readmeData = await fetchReadmeData();
setTheme( {
name: themeData.name.raw,
description: themeData.description.raw,
@ -62,6 +63,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {
author_uri: themeData.author_uri.raw,
tags_custom: themeData.tags.rendered,
screenshot: themeData.screenshot,
recommended_plugins: readmeData.recommended_plugins,
subfolder:
themeData.stylesheet.lastIndexOf( '/' ) > 1
? themeData.stylesheet.substring(

View file

@ -29,6 +29,30 @@ export async function fetchThemeJson() {
}
}

export async function fetchReadmeData() {
const fetchOptions = {
path: '/create-block-theme/v1/get-readme-data',
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};

try {
const response = await apiFetch( fetchOptions );
if ( ! response?.data || 'SUCCESS' !== response?.status ) {
throw new Error(
`Failed to fetch readme data: ${
response?.message || response?.status
}`
);
}
return response?.data;
} catch ( e ) {
// @todo: handle error
}
}

export async function postCreateThemeVariation( name ) {
return apiFetch( {
path: '/create-block-theme/v1/create-variation',

View file

@ -0,0 +1,50 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Test the add_or_update_section method of the Theme_Readme class.
*
* @package Create_Block_Theme
* @covers Theme_Readme::add_or_update_section
* @group readme
*
*/
class CBT_ThemeReadme_AddOrUpdateSection extends CBT_Theme_Readme_UnitTestCase {
public function test_add_or_update_section() {
$section_title = 'Test Section';
$section_content = 'Test content abc123';

// Add a new section.
$readme = Theme_Readme::add_or_update_section( $section_title, $section_content );

// Check if the section was added.
$this->assertStringContainsString( $section_title, $readme, 'The section title is missing.' );
$this->assertStringContainsString( $section_content, $readme, 'The section content is missing' );

// Update the section.
$section_content_updated = 'Updated content xyz890';

$readme = Theme_Readme::add_or_update_section( $section_title, $section_content_updated );

// Check if the old content was updated.
$this->assertStringNotContainsString( $section_content, $readme, 'The old content is still present.' );

// Check if the new content was added.
$this->assertStringContainsString( $section_title, $readme, 'The section title is missing.' );
$this->assertStringContainsString( $section_content_updated, $readme, 'The updated content is missing.' );

// Check if that the section title was added only once.
$section_count = substr_count( $readme, $section_title );
$this->assertEquals( 1, $section_count, 'The section title was added more than once.' );
}

public function test_add_or_update_section_with_no_content() {
$section_title = 'Test Section';
$section_content = '';

// Empty section should not be added.
$readme = Theme_Readme::add_or_update_section( $section_title, $section_content );
$this->assertStringNotContainsString( $section_title, $readme, 'The title of an empty section should not be added.' );
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* Base test case for Theme Readme tests.
*
* @package Create_Block_Theme
*/
abstract class CBT_Theme_Readme_UnitTestCase extends WP_UnitTestCase {

/**
* Stores the original active theme slug in order to restore it in tear down.
*
* @var string|null
*/
private $orig_active_theme_slug;

/**
* Stores the custom test theme directory.
*
* @var string|null;
*/
private $test_theme_dir;

/**
* Stores the original readme.txt content.
*
* @var string|null;
*/
private $orig_readme_content;

/**
* Sets up tests.
*/
public function set_up() {
parent::set_up();

// Store the original active theme.
$this->orig_active_theme_slug = get_option( 'stylesheet' );

// Create a test theme directory.
$this->test_theme_dir = DIR_TESTDATA . '/themes/';

// Register test theme directory.
register_theme_directory( $this->test_theme_dir );

// Switch to the test theme.
switch_theme( 'test-theme-readme' );

// Store the original readme.txt content.
$this->orig_readme_content = Theme_Readme::get_content();
}

/**
* Tears down tests.
*/
public function tear_down() {
parent::tear_down();

// Restore the original readme.txt content.
file_put_contents( Theme_Readme::file_path(), $this->orig_readme_content );

// Restore the original active theme.
switch_theme( $this->orig_active_theme_slug );
}
}

View file

@ -0,0 +1,115 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Test the create method of the Theme_Readme class.
*
* @package Create_Block_Theme
* @covers Theme_Readme::create
* @group readme
*/
class CBT_ThemeReadme_Create extends CBT_Theme_Readme_UnitTestCase {

/**
* @dataProvider data_test_create
*/
public function test_create( $data ) {
$readme = Theme_Readme::create( $data );

// Removes the newlines from the readme content to make it easier to search for strings.
$readme_without_newlines = str_replace( "\n", '', $readme );

$expected_name = '== ' . $data['name'] . ' ==';
$expected_description = '== Description ==' . $data['description'];
$expected_uri = 'Theme URI: ' . $data['uri'];
$expected_author = 'Contributors: ' . $data['author'];
$expected_author_uri = 'Author URI: ' . $data['author_uri'];
$expected_wp_version = 'Tested up to: ' . $data['wp_version'] ?? get_bloginfo( 'version' );
$expected_php_version = 'Requires PHP: ' . $data['required_php_version'];
$expected_license = 'License: ' . $data['license'];
$expected_license_uri = 'License URI: ' . $data['license_uri'];
$expected_image_credits = '== Images ==' . $data['image_credits'];
$expected_recommended_plugins = '== Recommended Plugins ==' . $data['recommended_plugins'];

$this->assertStringContainsString( $expected_name, $readme_without_newlines, 'The expected name is missing.' );
$this->assertStringContainsString( $expected_author, $readme_without_newlines, 'The expected author is missing.' );
$this->assertStringContainsString( $expected_wp_version, $readme_without_newlines, 'The expected WP version is missing.' );
$this->assertStringContainsString( $expected_image_credits, $readme_without_newlines, 'The expected image credits are missing.' );
$this->assertStringContainsString( $expected_recommended_plugins, $readme_without_newlines, 'The expected recommended plugins are missing.' );

// Assetion specific to child themes.
if ( isset( $data['is_child_theme'] ) && $data['is_child_theme'] ) {
$this->assertStringContainsString(
$data['name'] . ' is a child theme of Test Readme Theme (https://example.org/themes/test-readme-theme), (C) the WordPress team, [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)',
$readme_without_newlines,
'The expected reference to the parent theme is missing.'
);
}

// Assetion specific to child themes.
if ( isset( $data['is_cloned_theme'] ) && $data['is_cloned_theme'] ) {
$this->assertStringContainsString(
$data['name'] . ' is based on Test Readme Theme (https://example.org/themes/test-readme-theme), (C) the WordPress team, [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)',
$readme_without_newlines,
'The expected reference to the parent theme is missing.'
);
}
}

public function data_test_create() {
return array(
'complete data for a nomal theme' => array(
'data' => array(
'name' => 'My Theme',
'description' => 'New theme description',
'uri' => 'https://example.com',
'author' => 'New theme author',
'author_uri' => 'https://example.com/author',
'copyright_year' => '2077',
'wp_version' => '12.12',
'required_php_version' => '10.0',
'license' => 'GPLv2 or later',
'license_uri' => 'https://www.gnu.org/licenses/gpl-2.0.html',
'image_credits' => 'The images were taken from https://example.org and have a CC0 license.',
'recommended_plugins' => 'The theme is best used with the following plugins: Plugin 1, Plugin 2, Plugin 3.',
),
),
'complete data for a child theme' => array(
'data' => array(
'name' => 'My Child Theme',
'description' => 'New child theme description',
'uri' => 'https://example.com',
'author' => 'New theme author',
'author_uri' => 'https://example.com/author',
'copyright_year' => '2078',
'wp_version' => '13.13',
'required_php_version' => '11.0',
'license' => 'GPLv2 or later',
'license_uri' => 'https://www.gnu.org/licenses/gpl-2.0.html',
'image_credits' => 'The images were taken from https://example.org and have a CC0 license.',
'recommended_plugins' => 'The theme is best used with the following plugins: Plugin 1, Plugin 2, Plugin 3.',
'is_child_theme' => true,
),
),
'complete data for a cloned theme' => array(
'data' => array(
'name' => 'My Cloned Theme',
'description' => 'New cloned theme description',
'uri' => 'https://example.com',
'author' => 'New theme author',
'author_uri' => 'https://example.com/author',
'copyright_year' => '2079',
'wp_version' => '14.14',
'required_php_version' => '12.0',
'license' => 'GPLv2 or later',
'license_uri' => 'https://www.gnu.org/licenses/gpl-2.0.html',
'image_credits' => 'The images were taken from https://example.org and have a CC0 license.',
'recommended_plugins' => 'The theme is best used with the following plugins: Plugin 1, Plugin 2, Plugin 3.',
'is_cloned_theme' => true,
),
),
// TODO: Add more test cases.
);
}
}

View file

@ -0,0 +1,20 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Test the file_path method of the Theme_Readme class.
*
* @package Create_Block_Theme
* @covers Theme_Readme::file_path
* @group readme
*/
class CBT_ThemeReadme_FilePath extends CBT_Theme_Readme_UnitTestCase {
public function test_file_path() {
$result = Theme_Readme::file_path();
$expected = get_stylesheet_directory() . '/readme.txt';
$this->assertEquals( $expected, $result );

$this->assertEquals( 'test-theme-readme', get_option( 'stylesheet' ) );
}
}

View file

@ -0,0 +1,18 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Test the get_content method of the Theme_Readme class.
*
* @package Create_Block_Theme
* @covers Theme_Readme::get_content
* @group readme
*/
class CBT_ThemeReadme_GetContent extends CBT_Theme_Readme_UnitTestCase {
public function test_get_content() {
$result = Theme_Readme::get_content();
$expected = file_get_contents( Theme_Readme::file_path() );
$this->assertEquals( $expected, $result );
}
}

View file

@ -0,0 +1,49 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Test the update method of the Theme_Readme class.
*
* @package Create_Block_Theme
* @covers Theme_Readme::update
* @group readme
*/
class CBT_ThemeReadme_Update extends CBT_Theme_Readme_UnitTestCase {

/**
* @dataProvider data_test_update
*/
public function test_update( $data ) {
$readme_content = Theme_Readme::get_content();
$readme = Theme_Readme::update( $data, $readme_content );

// Removes the newlines from the readme content to make it easier to search for strings.
$readme_without_newlines = str_replace( "\n", '', $readme );

$expected_author = 'Contributors: ' . $data['author'];
$expected_wp_version = 'Tested up to: ' . $data['wp_version'] ?? get_bloginfo( 'version' );
$expected_image_credits = '== Images ==' . $data['image_credits'];
$expected_recommended_plugins = '== Recommended Plugins ==' . $data['recommended_plugins'];

$this->assertStringContainsString( $expected_author, $readme_without_newlines, 'The expected author is missing.' );
$this->assertStringContainsString( $expected_wp_version, $readme_without_newlines, 'The expected WP version is missing.' );
$this->assertStringContainsString( $expected_image_credits, $readme_without_newlines, 'The expected image credits are missing.' );
$this->assertStringContainsString( $expected_recommended_plugins, $readme_without_newlines, 'The expected recommended plugins are missing.' );
}

public function data_test_update() {
return array(
'complete data' => array(
'data' => array(
'description' => 'New theme description',
'author' => 'New theme author',
'wp_version' => '12.12',
'image_credits' => 'New image credits',
'recommended_plugins' => 'New recommended plugins',
),
),
// TODO: Add more test cases.
);
}
}

View file

@ -0,0 +1,54 @@
=== Test Readme Theme ===
Contributors: wordpressdotorg
Requires at least: 6.5
Tested up to: 6.5
Requires PHP: 7.0
Stable tag: 2.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html


== Description ==

Test Readme Theme is a test theme created with the sole purpose of testing the Create_Block_Theme_Readme class.


== Changelog ==

= 1.0 =
* Initial release

= 1.1 =
* Added new feature

= 2.0 =
* Added new major feature

= 2.1 =
* Added new feature


== Copyright ==

Test Readme Theme WordPress Theme, (C) 2023 WordPress.org
Test Readme Theme is distributed under the terms of the GNU GPL.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.


=== Images ===

License: CC0 https://creativecommons.org/publicdomain/zero/1.0/

museum.webp - https://www.rawpixel.com/image/3297419/free-photo-image-interior-hallway-architecture
tourist-and-building.webp - https://www.rawpixel.com/image/5928004/photo-image-public-domain-hand-person


View file

@ -0,0 +1,15 @@
/*
Theme Name: Test Readme Theme
Theme URI: https://example.org/themes/test-readme-theme
Author: the WordPress team
Author URI: https://wordpress.org
Description: Test Readme Theme is a theme for testing the readme.txt file reading/writing capabilities of the Create Block Theme plugin.
Requires at least: 6.4
Tested up to: 6.4
Requires PHP: 7.0
Version: 1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: testreadmetheme
Tags: one-column, custom-colors, custom-menu, custom-logo, editor-style, featured-images, full-site-editing, block-patterns, rtl-language-support, sticky-post, threaded-comments, translation-ready, wide-blocks, block-styles, style-variations, accessibility-ready, blog, portfolio, news
*/

View file

@ -0,0 +1,6 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"styles": {},
"settings": {}
}