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. // Add readme.txt.
$zip->addFromStringToTheme( $zip->addFromStringToTheme(
'readme.txt', 'readme.txt',
Theme_Readme::build_readme_txt( $theme ) Theme_Readme::create( $theme )
); );


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


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


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


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


// Add new metadata. // Add new metadata.

View file

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


public static function clone_current_theme( $theme ) { public static function clone_current_theme( $theme ) {

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


// Create theme directory. // Create theme directory.
$new_theme_path = get_theme_root() . DIRECTORY_SEPARATOR . $theme['slug']; $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_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 ); 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, '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 ) ); 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'] ) { if ( $theme['subfolder'] ) {
@ -61,7 +66,7 @@ class Theme_Create {
// Add readme.txt. // Add readme.txt.
file_put_contents( file_put_contents(
$new_theme_path . DIRECTORY_SEPARATOR . 'readme.txt', $new_theme_path . DIRECTORY_SEPARATOR . 'readme.txt',
Theme_Readme::build_readme_txt( $theme ) Theme_Readme::create( $theme )
); );


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


// Add new metadata. // Add new metadata.

View file

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


class Theme_Readme { 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 ) { public static function create( $theme ) {
$slug = $theme['slug']; $name = $theme['name'];
$name = $theme['name']; $description = $theme['description'] ?? '';
$description = $theme['description']; $uri = $theme['uri'] ?? '';
$uri = $theme['uri']; $author = $theme['author'] ?? '';
$author = $theme['author']; $author_uri = $theme['author_uri'] ?? '';
$author_uri = $theme['author_uri']; $copy_year = $theme['copyright_year'] ?? gmdate( 'Y' );
$copy_year = gmdate( 'Y' ); $wp_version = $theme['wp_version'] ?? get_bloginfo( 'version' );
$wp_version = get_bloginfo( 'version' ); $required_php_version = $theme['required_php_version'] ?? '5.7';
$image_credits = $theme['image_credits'] ?? ''; $license = $theme['license'] ?? 'GPLv2 or later';
$recommended_plugins = $theme['recommended_plugins'] ?? ''; $license_uri = $theme['license_uri'] ?? 'http://www.gnu.org/licenses/gpl-2.0.html';
$is_parent_theme = $theme['is_parent_theme'] ?? false; $image_credits = $theme['image_credits'] ?? '';
$original_theme = $theme['original_theme'] ?? ''; $recommended_plugins = $theme['recommended_plugins'] ?? '';
$is_child_theme = $theme['is_child_theme'] ?? false;


// Handle copyright section. // Generates the copyright section text.
$new_copyright_section = $is_parent_theme || $original_theme ? true : false; $copyright_section_content = self::get_copyright_text( $theme );
$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 );


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


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

== Description ==

{$description}

== Changelog ==

= 1.0 =
* Initial release
{$recommended_plugins_section}
{$copyright_section}
"; ";
$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. * Get the theme data from the installed theme.
* Used in readme.txt of cloned themes.
* *
* @param string $new_name New theme name. * @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 ) { private static function get_active_theme_data() {
if ( ! $new_name ) {
return;
}

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


if ( ! $readme_content ) { return array(
return; 'name' => $original_name,
} 'uri' => $original_uri,

'author' => $original_author,
// Get license from original theme readme.txt 'license' => $original_license,
if ( str_contains( $readme_content, 'License:' ) ) { 'license_uri' => $original_license_uri,
$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
); );

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. * Build default copyright text for a theme.
* Used in readme.txt of cloned themes or child themes.
* *
* @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 ) { private static function get_copyright_text( $theme ) {
// Default copyright section. $name = $theme['name'];
$copyright_section = "== Copyright == $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. {$name} is distributed under the terms of the GNU GPL.


This program is free software: you can redistribute it and/or modify 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, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 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. $is_child_theme = $theme['is_child_theme'] ?? false;
if ( $new_copyright_section ) { $is_cloned_theme = $theme['is_cloned_theme'] ?? false;
$copyright_section_intro = '== Copyright ==';


// Get current theme readme.txt /*
$current_readme = get_stylesheet_directory() . '/readme.txt' ?? ''; * If the theme is a child theme or a cloned theme, add a reference to the parent theme.
$current_readme_content = file_exists( $current_readme ) ? file_get_contents( $current_readme ) : ''; *
* 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 ) { $reference_string = $is_child_theme
return; ? '%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 $reference = sprintf(
if ( str_contains( $current_readme_content, $copyright_section_intro ) ) { $reference_string,
$copyright_section_start = strpos( $current_readme_content, $copyright_section_intro ); $name,
$copyright_section = substr( $current_readme_content, $copyright_section_start ); $original_theme['name'],
$original_theme['uri'],
$original_theme['author'],
$original_theme['license'],
$original_theme['license_uri']
);


if ( $original_theme_credits ) { $text .= "\n\n" . $reference;
$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;
}
}
} }


if ( $image_credits ) { return $text;
$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;
} }


/** /**
* Update current readme.txt file, rather than building a new one. * 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 * @return string
*/ */
public static function update_readme_txt( $theme ) { public static function update( $theme, $readme_content = '' ) {
$description = $theme['description']; // Theme data.
$author = $theme['author']; $description = $theme['description'] ?? '';
$wp_version = get_bloginfo( 'version' ); $author = $theme['author'] ?? '';
$wp_version = $theme['wp_version'] ?? get_bloginfo( 'version' );
$image_credits = $theme['image_credits'] ?? ''; $image_credits = $theme['image_credits'] ?? '';
$recommended_plugins = $theme['recommended_plugins'] ?? ''; $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. // Update description.
if ( $description ) { $readme_content = self::add_or_update_section( 'Description', $description, $readme_content );
$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 );
}


// Update Author/Contributors. // Update Author/Contributors.
if ( $author ) { $readme_content = self::add_or_update_prop( 'Contributors', $author, $readme_content );
$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 );
}


// Update "Tested up to" version. // Update "Tested up to" version.
if ( $wp_version ) { $readme_content = self::add_or_update_prop( 'Tested up to', $wp_version, $readme_content );
$pattern = '/(Tested up to:)(.*?)(\n|$)/s';
preg_match_all( $pattern, $updated_readme, $matches ); // Update recommended plugins section.
$current_uri = $matches[0][0]; $readme_content = self::add_or_update_section( 'Recommended Plugins', $recommended_plugins, $readme_content );
$updated_readme = str_replace( $current_uri, "Tested up to: {$wp_version}\n", $updated_readme );
// 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 ) { $section_content = trim( $section_content, "\r" );
$updated_readme = self::recommended_plugins_section( $recommended_plugins, $updated_readme ); $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 ) { return $updated_content;
$updated_readme = $updated_readme . "\n\n" . $image_credits; }
}


/**
* 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; 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. * 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 ) { function rest_get_readme_data( $request ) {
try { try {
$readme_data = Theme_Utils::get_readme_data(); $readme_data = Theme_Readme::get_sections();

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


function rest_create_child_theme( $request ) { 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 //TODO: Handle screenshots
$screenshot = null; $screenshot = null;


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


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


// Build style.css with new theme metadata // Build style.css with new theme metadata
@ -407,16 +409,16 @@ class Create_Block_Theme_API {
* Update the theme metadata and relocate the theme. * Update the theme metadata and relocate the theme.
*/ */
function rest_update_theme( $request ) { 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 // Update the metadata of the theme in the style.css file
$style_css = file_get_contents( get_stylesheet_directory() . '/style.css' ); $style_css = file_get_contents( get_stylesheet_directory() . '/style.css' );
$style_css = Theme_Styles::update_style_css( $style_css, $theme ); $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() . '/style.css', $style_css );
file_put_contents(
get_stylesheet_directory() . '/readme.txt', $readme_content = Theme_Readme::get_content();
Theme_Readme::update_readme_txt( $theme ) $readme_content = Theme_Readme::update( $theme, $readme_content );
); file_put_contents( Theme_Readme::file_path(), $readme_content );


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

View file

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

View file

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


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


useSelect( async ( select ) => { useSelect( async ( select ) => {
const themeData = select( 'core' ).getCurrentTheme(); const themeData = select( 'core' ).getCurrentTheme();
const readmeData = await fetchReadmeData();
setTheme( { setTheme( {
name: themeData.name.raw, name: themeData.name.raw,
description: themeData.description.raw, description: themeData.description.raw,
@ -62,6 +63,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {
author_uri: themeData.author_uri.raw, author_uri: themeData.author_uri.raw,
tags_custom: themeData.tags.rendered, tags_custom: themeData.tags.rendered,
screenshot: themeData.screenshot, screenshot: themeData.screenshot,
recommended_plugins: readmeData.recommended_plugins,
subfolder: subfolder:
themeData.stylesheet.lastIndexOf( '/' ) > 1 themeData.stylesheet.lastIndexOf( '/' ) > 1
? themeData.stylesheet.substring( ? 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 ) { export async function postCreateThemeVariation( name ) {
return apiFetch( { return apiFetch( {
path: '/create-block-theme/v1/create-variation', 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": {}
}