Merge branch 'trunk' into add/handle-google-font-credits

This commit is contained in:
Sarah Norris 2023-04-27 15:52:17 +01:00
commit 2b4348f11e
33 changed files with 5545 additions and 3082 deletions

1
.gitignore vendored
View file

@ -25,6 +25,7 @@ vendor/
# ignore system files
.DS_Store
.vscode
# plugin build folder
build/

View file

@ -29,6 +29,28 @@ class Create_Block_Theme_Admin {
public function __construct() {
add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
add_action( 'admin_init', array( $this, 'blockbase_save_theme' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'create_block_theme_enqueue' ) );
add_action( 'rest_api_init', array( $this, 'register_theme_export' ) );
}
function create_block_theme_enqueue() {
global $pagenow;
if ( 'site-editor.php' !== $pagenow ) {
return;
}
$asset_file = include( plugin_dir_path( dirname( __FILE__ ) ) . 'build/plugin-sidebar.asset.php' );
wp_register_script(
'create-block-theme-slot-fill',
plugins_url( 'build/plugin-sidebar.js', dirname( __FILE__ ) ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_enqueue_script(
'create-block-theme-slot-fill',
);
}
function create_admin_menu() {
@ -55,14 +77,14 @@ class Create_Block_Theme_Admin {
* Export activated child theme
*/
function export_child_theme( $theme ) {
$theme['slug'] = wp_get_theme()->get( 'TextDomain' );
$theme['slug'] = Theme_Utils::get_theme_slug( $theme['name'] );
// Create ZIP file in the temporary directory.
$filename = tempnam( get_temp_dir(), $theme['slug'] );
$zip = Theme_Zip::create_zip( $filename );
$zip = Theme_Zip::copy_theme_to_zip( $zip, null, null );
$zip = Theme_Zip::add_templates_to_zip( $zip, 'current', null );
$zip = Theme_Zip::add_templates_to_zip( $zip, 'current', $theme['slug'] );
$zip = Theme_Zip::add_theme_json_to_zip( $zip, 'current' );
$zip->close();
@ -79,6 +101,8 @@ class Create_Block_Theme_Admin {
* Create a sibling theme of the activated theme
*/
function create_sibling_theme( $theme, $screenshot ) {
$theme_slug = Theme_Utils::get_theme_slug( $theme['name'] );
// Sanitize inputs.
$theme['name'] = sanitize_text_field( $theme['name'] );
$theme['description'] = sanitize_text_field( $theme['description'] );
@ -86,8 +110,9 @@ class Create_Block_Theme_Admin {
$theme['author'] = sanitize_text_field( $theme['author'] );
$theme['author_uri'] = sanitize_text_field( $theme['author_uri'] );
$theme['tags_custom'] = sanitize_text_field( $theme['tags_custom'] );
$theme['slug'] = Theme_Utils::get_theme_slug( $theme['name'] );
$theme['slug'] = $theme_slug;
$theme['template'] = wp_get_theme()->get( 'Template' );
$theme['text_domain'] = $theme_slug;
// Create ZIP file in the temporary directory.
$filename = tempnam( get_temp_dir(), $theme['slug'] );
@ -136,6 +161,8 @@ class Create_Block_Theme_Admin {
* Clone the activated theme to create a new theme
*/
function clone_theme( $theme, $screenshot ) {
$theme_slug = Theme_Utils::get_theme_slug( $theme['name'] );
// Sanitize inputs.
$theme['name'] = sanitize_text_field( $theme['name'] );
$theme['description'] = sanitize_text_field( $theme['description'] );
@ -143,9 +170,15 @@ class Create_Block_Theme_Admin {
$theme['author'] = sanitize_text_field( $theme['author'] );
$theme['author_uri'] = sanitize_text_field( $theme['author_uri'] );
$theme['tags_custom'] = sanitize_text_field( $theme['tags_custom'] );
$theme['slug'] = Theme_Utils::get_theme_slug( $theme['name'] );
$theme['template'] = wp_get_theme()->get( 'Template' );
$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.
if ( empty( $theme['tags_custom'] ) ) {
$theme['tags_custom'] = implode( ', ', wp_get_theme()->get( 'Tags' ) );
}
// Create ZIP file in the temporary directory.
$filename = tempnam( get_temp_dir(), $theme['slug'] );
@ -187,13 +220,38 @@ class Create_Block_Theme_Admin {
header( 'Content-Disposition: attachment; filename=' . $theme['slug'] . '.zip' );
header( 'Content-Length: ' . filesize( $filename ) );
flush();
echo readfile( $filename );
die();
readfile( $filename );
unlink( $filename );
exit;
}
function rest_export_theme( $request ) {
$theme = $request->get_params();
$this->clone_theme( $theme, null );
}
public function register_theme_export() {
register_rest_route(
'create-block-theme/v1',
'/export',
array(
'methods' => 'POST',
'callback' => array( $this, 'rest_export_theme' ),
'permission_callback' => function () {
return current_user_can( 'edit_theme_options' );
},
)
);
}
/**
* Create a child theme of the activated theme
*/
function create_child_theme( $theme, $screenshot ) {
$parent_theme_slug = Theme_Utils::get_theme_slug( wp_get_theme()->get( 'Name' ) );
$child_theme_slug = Theme_Utils::get_theme_slug( $theme['name'] );
// Sanitize inputs.
$theme['name'] = sanitize_text_field( $theme['name'] );
$theme['description'] = sanitize_text_field( $theme['description'] );
@ -201,14 +259,16 @@ class Create_Block_Theme_Admin {
$theme['author'] = sanitize_text_field( $theme['author'] );
$theme['author_uri'] = sanitize_text_field( $theme['author_uri'] );
$theme['tags_custom'] = sanitize_text_field( $theme['tags_custom'] );
$theme['slug'] = Theme_Utils::get_theme_slug( $theme['name'] );
$theme['template'] = wp_get_theme()->get( 'TextDomain' );
$theme['text_domain'] = $child_theme_slug;
$theme['template'] = $parent_theme_slug;
$theme['slug'] = $child_theme_slug;
// Create ZIP file in the temporary directory.
$filename = tempnam( get_temp_dir(), $theme['slug'] );
$zip = Theme_Zip::create_zip( $filename );
$zip = Theme_Zip::add_templates_to_zip( $zip, 'user', null );
$zip = Theme_Zip::add_templates_to_zip( $zip, 'user', $theme['slug'] );
$zip = Theme_Zip::add_theme_json_to_zip( $zip, 'user' );
// Add readme.txt.
@ -266,6 +326,8 @@ class Create_Block_Theme_Admin {
}
function create_blank_theme( $theme, $screenshot ) {
$theme_slug = Theme_Utils::get_theme_slug( $theme['name'] );
// Sanitize inputs.
$theme['name'] = sanitize_text_field( $theme['name'] );
$theme['description'] = sanitize_text_field( $theme['description'] );
@ -274,7 +336,8 @@ class Create_Block_Theme_Admin {
$theme['author_uri'] = sanitize_text_field( $theme['author_uri'] );
$theme['tags_custom'] = sanitize_text_field( $theme['tags_custom'] );
$theme['template'] = '';
$theme['slug'] = Theme_Utils::get_theme_slug( $theme['name'] );
$theme['slug'] = $theme_slug;
$theme['text_domain'] = $theme_slug;
// Create theme directory.
$source = plugin_dir_path( __DIR__ ) . 'assets/boilerplate';

32
admin/class-react-app.php Normal file
View file

@ -0,0 +1,32 @@
<?php
class React_App {
public static function bootstrap() {
// Load the required WordPress packages.
// Automatically load imported dependencies and assets version.
$asset_file = include plugin_dir_path( __DIR__ ) . 'build/index.asset.php';
// Enqueue CSS dependencies of the scripts included in the build.
foreach ( $asset_file['dependencies'] as $style ) {
wp_enqueue_style( $style );
}
// Enqueue CSS of the app
wp_enqueue_style( 'create-block-theme-app', plugins_url( 'build/index.css', __DIR__ ), array(), $asset_file['version'] );
// Load our app.js.
array_push( $asset_file['dependencies'], 'wp-i18n' );
wp_enqueue_script( 'create-block-theme-app', plugins_url( 'build/index.js', __DIR__ ), $asset_file['dependencies'], $asset_file['version'] );
// Set google fonts json file url.
wp_localize_script(
'create-block-theme-app',
'createBlockTheme',
array(
'googleFontsDataUrl' => plugins_url( 'assets/google-fonts/fallback-fonts-list.json', __DIR__ ),
'adminUrl' => admin_url(),
'themeUrl' => get_stylesheet_directory_uri(),
)
);
}
}

View file

@ -41,7 +41,7 @@ class Form_Messages {
printf(
// translators: %1$s: Theme name
esc_html__( 'Blank theme created, head over to Appearance > Themes to activate %1$s', 'create-block-theme' ),
esc_html( $theme_name )
esc_html( stripslashes( $theme_name ) )
);
?>
</p>

View file

@ -188,4 +188,5 @@ class Theme_Blocks {
$markup = html_entity_decode( $markup, ENT_QUOTES | ENT_XML1, 'UTF-8' );
return $markup;
}
}

View file

@ -53,8 +53,7 @@ class Theme_Form {
</label>
<br />
<?php _e( '[Create a new theme cloning the activated child theme. The parent theme will be the same as the parent of the currently activated theme. The resulting theme will have all of the assets of the activated theme, none of the assets provided by the parent theme, as well as user changes.]', 'create-block-theme' ); ?>
<p><b><?php _e( 'NOTE: Sibling themes created from this theme will have the original namespacing. This should be changed manually once the theme has been created.', 'create-block-theme' ); ?></b></p>
<br />
<br /><br />
<?php else : ?>
<label>
<input value="child" type="radio" name="theme[type]" class="regular-text code" onchange="toggleForm( this );"/>
@ -132,7 +131,7 @@ class Theme_Form {
<p><em><?php _e( 'Items indicated with (*) are required.', 'create-block-theme' ); ?></em></p>
<label>
<?php _e( 'Theme Name (*):', 'create-block-theme' ); ?><br />
<input placeholder="<?php _e( 'Theme Name', 'create-block-theme' ); ?>" type="text" name="theme[name]" class="large-text" />
<input placeholder="<?php _e( 'Theme Name', 'create-block-theme' ); ?>" type="text" name="theme[name]" class="large-text" id="theme-name" autocomplete="off" />
</label>
<br /><br />
<label>

View file

@ -137,15 +137,15 @@ class Theme_Media {
public static function add_media_to_local( $media ) {
foreach ( $media as $url ) {
$download_file = file_get_contents( $url );
$download_file = download_url( $url );
// TODO: implement a warning if the file is missing
if ( ! is_wp_error( $download_file ) ) {
$media_path = get_stylesheet_directory() . DIRECTORY_SEPARATOR . self::get_media_folder_path_from_url( $url );
if ( ! is_dir( $media_path ) ) {
wp_mkdir_p( $media_path );
}
file_put_contents(
$media_path . basename( $url ),
$download_file
);
rename( $download_file, $media_path . basename( $url ) );
}
}
}
}

View file

@ -1,8 +1,8 @@
<?php
class Theme_Patterns {
public static function pattern_from_template( $template ) {
$theme_slug = wp_get_theme()->get( 'TextDomain' );
public static function pattern_from_template( $template, $new_slug = null ) {
$theme_slug = $new_slug ? $new_slug : wp_get_theme()->get( 'TextDomain' );
$pattern_slug = $theme_slug . '/' . $template->slug;
$pattern_content = (
'<?php
@ -61,4 +61,10 @@ class Theme_Patterns {
return "<?php echo esc_attr_e( '" . $text . "', '" . wp_get_theme()->get( 'Name' ) . "' ); ?>";
}
}
public static function create_pattern_link( $attributes ) {
$block_attributes = array_filter( $attributes );
$attributes_json = json_encode( $block_attributes, JSON_UNESCAPED_SLASHES );
return '<!-- wp:pattern ' . $attributes_json . ' /-->';
}
}

View file

@ -12,6 +12,7 @@ class Theme_Readme {
$author = $theme['author'];
$author_uri = $theme['author_uri'];
$copy_year = gmdate( 'Y' );
$wp_version = get_bloginfo( 'version' );
$original_theme = $theme['original_theme'] ?? '';
$original_theme_credits = $original_theme ? self::original_theme_credits( $name ) : '';
@ -24,7 +25,7 @@ class Theme_Readme {
return "=== {$name} ===
Contributors: {$author}
Requires at least: 5.8
Tested up to: 5.9
Tested up to: {$wp_version}
Requires PHP: 5.7
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

View file

@ -8,29 +8,39 @@ class Theme_Styles {
*/
public static function build_child_style_css( $theme ) {
$slug = $theme['slug'];
$name = $theme['name'];
$description = $theme['description'];
$name = stripslashes( $theme['name'] );
$description = stripslashes( $theme['description'] );
$uri = $theme['uri'];
$author = $theme['author'];
$author = stripslashes( $theme['author'] );
$author_uri = $theme['author_uri'];
$wp_version = get_bloginfo( 'version' );
$template = $theme['template'];
$text_domain = $theme['text_domain'];
$tags = Theme_Tags::theme_tags_list( $theme );
return "/*
$style_css = "/*
Theme Name: {$name}
Theme URI: {$uri}
Author: {$author}
Author URI: {$author_uri}
Description: {$description}
Requires at least: 5.8
Tested up to: 5.9
Tested up to: {$wp_version}
Requires PHP: 5.7
Version: 0.0.1
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Template: {$template}
Text Domain: {$slug}
";
if ( ! empty( $template ) ) {
$style_css .= "Template: {$template}\n";
}
$style_css .= "Text Domain: {$text_domain}
Tags: {$tags}
*/";
return $style_css;
}
public static function clear_user_styles_customizations() {

View file

@ -14,17 +14,15 @@ class Theme_Tags {
* @since 1.5.2
*/
public static function theme_tags_list( $theme ) {
$checkbox_tags_merged = array_merge( $theme['tags-subject'] ?? array(), $theme['tags-layout'] ?? array(), $theme['tags-features'] ?? array() );
$checkbox_tags = $checkbox_tags_merged ? ', ' . implode( ', ', $checkbox_tags_merged ) : '';
$custom_tags = $theme['tags_custom'] ? ', ' . $theme['tags_custom'] : '';
$tags = $checkbox_tags . $custom_tags;
$checkbox_tags_merged = array_merge(
$theme['tags-subject'] ?? array(),
$theme['tags-layout'] ?? array(),
$theme['tags-features'] ?? array(),
);
$custom_tags = array_map( 'trim', explode( ',', $theme['tags_custom'] ?? '' ) );
$tags = array_unique( array_merge( $checkbox_tags_merged, $custom_tags ) );
// Remove comma and space from start of tags list
if ( ', ' === substr( $tags, 0, 2 ) ) {
$tags = substr( $tags, 2 );
}
return $tags;
return implode( ', ', $tags );
}
/**

View file

@ -8,9 +8,7 @@ class Theme_Templates {
/*
* Build a collection of templates and template-parts that should be exported (and modified) based on the given export_type and new slug
*/
public static function get_theme_templates( $export_type, $new_slug ) {
$old_slug = wp_get_theme()->get( 'TextDomain' );
public static function get_theme_templates( $export_type ) {
$templates = get_block_templates();
$template_parts = get_block_templates( array(), 'wp_template_part' );
$exported_templates = array();
@ -25,9 +23,7 @@ class Theme_Templates {
$template = self::filter_theme_template(
$template,
$export_type,
$templates_path,
$old_slug,
$new_slug
$templates_path
);
if ( $template ) {
$exported_templates[] = $template;
@ -38,9 +34,7 @@ class Theme_Templates {
$template = self::filter_theme_template(
$template,
$export_type,
$parts_path,
$old_slug,
$new_slug
$parts_path
);
if ( $template ) {
$exported_parts[] = $template;
@ -59,7 +53,7 @@ class Theme_Templates {
* Templates not filtered out are modified based on the slug information provided and cleaned up
* to have the expected exported value.
*/
static function filter_theme_template( $template, $export_type, $path, $old_slug, $new_slug ) {
static function filter_theme_template( $template, $export_type, $path ) {
if ( 'theme' === $template->source && 'user' === $export_type ) {
return false;
}
@ -75,10 +69,14 @@ class Theme_Templates {
// This replaces that with dashes again. We should consider decoding the entire string but that is proving difficult.
$template->content = str_replace( '\u002d', '-', $template->content );
return $template;
}
public static function replace_template_namespace( $template, $new_slug ) {
$old_slug = wp_get_theme()->get( 'TextDomain' );
if ( $new_slug ) {
$template->content = str_replace( $old_slug, $new_slug, $template->content );
}
return $template;
}
@ -103,7 +101,7 @@ class Theme_Templates {
public static function add_templates_to_local( $export_type ) {
$theme_templates = self::get_theme_templates( $export_type, null );
$theme_templates = self::get_theme_templates( $export_type );
$template_folders = get_block_theme_folders();
// If there is no templates folder, create it.
@ -123,7 +121,10 @@ class Theme_Templates {
// If there are external images, add it as a pattern
$pattern = Theme_Patterns::pattern_from_template( $template_data );
$template_data->content = '<!-- wp:pattern {"slug":"' . $pattern['slug'] . '"} /-->';
$pattern_link_attributes = array(
'slug' => $pattern['slug'],
);
$template_data->content = Theme_Patterns::create_pattern_link( $pattern_link_attributes );
// Write the pattern
file_put_contents(
@ -160,7 +161,10 @@ class Theme_Templates {
// If there are external images, add it as a pattern
$pattern = Theme_Patterns::pattern_from_template( $template_data );
$template_data->content = '<!-- wp:pattern {"slug":"' . $pattern['slug'] . '"} /-->';
$pattern_link_attributes = array(
'slug' => $pattern['slug'],
);
$template_data->content = Theme_Patterns::create_pattern_link( $pattern_link_attributes );
// Write the pattern
file_put_contents(

View file

@ -89,7 +89,7 @@ class Theme_Zip {
* all = all templates no matter what
*/
public static function add_templates_to_zip( $zip, $export_type, $new_slug ) {
$theme_templates = Theme_Templates::get_theme_templates( $export_type, $new_slug );
$theme_templates = Theme_Templates::get_theme_templates( $export_type );
if ( $theme_templates->templates ) {
$zip->addEmptyDir( 'templates' );
@ -101,11 +101,15 @@ class Theme_Zip {
foreach ( $theme_templates->templates as $template ) {
$template_data = Theme_Blocks::make_template_images_local( $template );
$template_data = Theme_Templates::replace_template_namespace( $template_data, $new_slug );
// If there are images in the template, add it as a pattern
if ( count( $template_data->media ) > 0 ) {
$pattern = Theme_Patterns::pattern_from_template( $template_data );
$template_data->content = '<!-- wp:pattern {"slug":"' . $pattern['slug'] . '"} /-->';
$pattern = Theme_Patterns::pattern_from_template( $template_data, $new_slug );
$pattern_link_attributes = array(
'slug' => $pattern['slug'],
);
$template_data->content = Theme_Patterns::create_pattern_link( $pattern_link_attributes );
// Add pattern to zip
$zip->addFromString(
@ -127,11 +131,15 @@ class Theme_Zip {
foreach ( $theme_templates->parts as $template_part ) {
$template_data = Theme_Blocks::make_template_images_local( $template_part );
$template_data = Theme_Templates::replace_template_namespace( $template_data, $new_slug );
// If there are images in the template, add it as a pattern
if ( count( $template_data->media ) > 0 ) {
$pattern = Theme_Patterns::pattern_from_template( $template_data );
$template_data->content = '<!-- wp:pattern {"slug":"' . $pattern['slug'] . '"} /-->';
$pattern = Theme_Patterns::pattern_from_template( $template_data, $new_slug );
$pattern_link_attributes = array(
'slug' => $pattern['slug'],
);
$template_data->content = Theme_Patterns::create_pattern_link( $pattern_link_attributes );
// Add pattern to zip
$zip->addFromString(
@ -154,11 +162,21 @@ class Theme_Zip {
}
static function add_media_to_zip( $zip, $media ) {
if ( ! function_exists( 'download_url' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$media = array_unique( $media );
foreach ( $media as $url ) {
$folder_path = Theme_Media::get_media_folder_path_from_url( $url );
$download_file = file_get_contents( $url );
$zip->addFromString( $folder_path . basename( $url ), $download_file );
$download_file = download_url( $url );
// If there was an error downloading the file, skip it.
// TODO: Implement a warning if the file is missing
if ( ! is_wp_error( $download_file ) ) {
$content_array = file( $download_file );
$file_as_string = implode( '', $content_array );
$zip->addFromString( $folder_path . basename( $url ), $file_as_string );
}
}
}

View file

@ -1,3 +1,5 @@
const { __ } = wp.i18n;
// Toggles the visibility of the forms based on the selected theme type
// eslint-disable-next-line no-unused-vars
function toggleForm( element ) {
@ -9,11 +11,13 @@ function toggleForm( element ) {
case 'export':
case 'save':
// Forms should stay hidden
resetThemeName();
break;
case 'child':
case 'clone':
case 'blank':
case 'sibling':
// Show New Theme form
document
.getElementById( 'new_theme_metadata_form' )
@ -93,12 +97,122 @@ function limitCheckboxSelection( checkboxesSelector, max = 0 ) {
// Store active theme tags when page is loaded
let activeThemeTags = [];
window.onload = () => {
function onWindowLoad() {
activeThemeTags = document.querySelectorAll(
'.theme-tags input[type="checkbox"]:checked'
);
}
window.addEventListener( 'load', onWindowLoad );
window.addEventListener( 'load', prepareThemeNameValidation );
function prepareThemeNameValidation() {
const themeNameInput = document.getElementById( 'theme-name' );
themeNameInput.addEventListener( 'input', validateThemeNameInput );
}
function slugify( text ) {
// Removes spaces
return text.toLowerCase().replace( / /g, '' );
}
function slugifyUnderscores( text ) {
// Replaces spaces with underscores
return text.toLowerCase().replace( / /g, '_' );
}
function slugifyDashes( text ) {
// Replaces spaces with dashes
return text.toLowerCase().replace( / /g, '-' );
}
function slugifyNoDashes( text ) {
// Removes spaces, dashes, and underscores
return text.toLowerCase().replace( / /g, '' ).replace( /[-_]/g, '' );
}
const ERROR_NAME_NOT_AVAILABLE = __(
'Theme name is not available in the WordPress.org theme directory',
'create-block-theme'
);
const ERROR_NAME_CONTAINS_THEME = __(
'Theme name cannot contain the word "theme"',
'create-block-theme'
);
const ERROR_NAME_CONTAINS_WORDPRESS = __(
'Theme name cannot contain the word "WordPress"',
'create-block-theme'
);
function isThemeNameValid( themeName ) {
// Check the validity of the theme name following the WordPress.org theme directory rules
// https://meta.svn.wordpress.org/sites/trunk/wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-upload.php
/* eslint-disable @wordpress/no-unused-vars-before-return */
const lowerCaseName = themeName.toLowerCase();
const slug = slugify( themeName );
const slugDashes = slugifyUnderscores( themeName );
const slugUnderscores = slugifyDashes( themeName );
const slugNoDashes = slugifyNoDashes( themeName );
const validityStatus = {
isValid: true,
errorMessage: '',
};
// Check if the theme contains the word theme
if ( lowerCaseName.includes( 'theme' ) ) {
validityStatus.isValid = false;
validityStatus.errorMessage = ERROR_NAME_CONTAINS_THEME;
return validityStatus;
}
// Check if the theme name contains WordPress
if ( slugNoDashes.includes( 'wordpress' ) ) {
validityStatus.isValid = false;
validityStatus.errorMessage = ERROR_NAME_CONTAINS_WORDPRESS;
return validityStatus;
}
// Check if the theme name is available
const isNameAvailable = () => {
// default to empty array if the unavailable theme names are not loaded yet from the API
const notAvailableSlugs = wpOrgThemeDirectory.themeSlugs || [];
// Compare the theme name to the list of unavailable theme names using several different slug formats
return ! notAvailableSlugs.some(
( s ) =>
s === slug ||
s === slugDashes ||
s === slugUnderscores ||
slugifyNoDashes( s ) === slugNoDashes
);
};
if ( ! isNameAvailable() ) {
validityStatus.isValid = false;
validityStatus.errorMessage = ERROR_NAME_NOT_AVAILABLE;
return validityStatus;
}
return validityStatus;
}
function validateThemeNameInput() {
const themeName = this?.value;
if ( ! themeName ) return true;
// Check if theme name is available
const validityStatus = isThemeNameValid( themeName );
if ( ! validityStatus.isValid ) {
this.setCustomValidity( validityStatus.errorMessage );
this.reportValidity();
} else {
this.setCustomValidity( '' );
}
}
// Resets all theme tag states (checked, disabled) to default values
function resetThemeTags( themeType ) {
// Clear all checkboxes
@ -127,3 +241,9 @@ function resetThemeTags( themeType ) {
} );
}
}
function resetThemeName() {
const themeNameInput = document.getElementById( 'theme-name' );
themeNameInput.value = '';
themeNameInput.setCustomValidity( '' );
}

View file

@ -1,8 +1,10 @@
<?php
require_once( dirname( __DIR__ ) . '/class-react-app.php' );
class Fonts_Page {
public static function manage_fonts_admin_page() {
self::load_fonts_react_app();
React_App::bootstrap();
$theme_name = wp_get_theme()->get( 'Name' );
@ -25,37 +27,8 @@ class Fonts_Page {
?>
<p name="theme-fonts-json" id="theme-fonts-json" class="hidden"><?php echo $fonts_json_string; ?></p>
<div id="fonts-app"></div>
<div id="create-block-theme-app"></div>
<input type="hidden" name="nonce" id="nonce" value="<?php echo wp_create_nonce( 'create_block_theme' ); ?>" />
<?php
}
public static function load_fonts_react_app() {
// Load the required WordPress packages.
// Automatically load imported dependencies and assets version.
$asset_file = include plugin_dir_path( dirname( __DIR__ ) ) . 'build/index.asset.php';
// Enqueue CSS dependencies of the scripts included in the build.
foreach ( $asset_file['dependencies'] as $style ) {
wp_enqueue_style( $style );
}
// Enqueue CSS of the app
wp_enqueue_style( 'fonts-app', plugins_url( 'build/index.css', dirname( __DIR__ ) ), array(), $asset_file['version'] );
// Load our app.js.
array_push( $asset_file['dependencies'], 'wp-i18n' );
wp_enqueue_script( 'create-block-theme-app', plugins_url( 'build/index.js', dirname( __DIR__ ) ), $asset_file['dependencies'], $asset_file['version'] );
// Set google fonts json file url.
wp_localize_script(
'create-block-theme-app',
'createBlockTheme',
array(
'googleFontsDataUrl' => plugins_url( 'assets/google-fonts/fallback-fonts-list.json', dirname( __DIR__ ) ),
'adminUrl' => admin_url(),
'themeUrl' => get_stylesheet_directory_uri(),
)
);
}
}

View file

@ -1,13 +1,13 @@
<?php
require_once( __DIR__ . '/fonts-page.php' );
require_once( dirname( __DIR__ ) . '/class-react-app.php' );
class Google_Fonts {
public static function google_fonts_admin_page() {
Fonts_Page::load_fonts_react_app();
React_App::bootstrap();
?>
<input id="nonce" type="hidden" value="<?php echo wp_create_nonce( 'create_block_theme' ); ?>" />
<div id="fonts-app"></div>
<div id="create-block-theme-app"></div>
<?php
}

View file

@ -1,6 +1,6 @@
<?php
require_once( __DIR__ . '/fonts-page.php' );
require_once( dirname( __DIR__ ) . '/class-react-app.php' );
class Local_Fonts {
public static function local_fonts_admin_page() {
@ -8,11 +8,11 @@ class Local_Fonts {
wp_enqueue_script( 'inflate', plugin_dir_url( dirname( __FILE__ ) ) . 'js/lib/inflate.js', array(), '', false );
wp_enqueue_script( 'unbrotli', plugin_dir_url( dirname( __FILE__ ) ) . 'js/lib/unbrotli.js', array(), '', false );
Fonts_Page::load_fonts_react_app();
React_App::bootstrap();
?>
<input id="nonce" type="hidden" value="<?php echo wp_create_nonce( 'create_block_theme' ); ?>" />
<div id="fonts-app"></div>
<div id="create-block-theme-app"></div>
<?php
}

View file

@ -0,0 +1,75 @@
<?php
class WP_Theme_Directory {
const THEME_NAMES_ENDPOINT = 'https://themes.svn.wordpress.org/';
/**
* Initialize the class and set its properties.
*/
public function __construct() {
add_action( 'rest_api_init', array( $this, 'register_theme_names_endpoint' ) );
add_action( 'admin_init', array( $this, 'assets_enqueue' ) );
}
public static function register_theme_names_endpoint() {
register_rest_route(
'create-block-theme/v1',
'/wp-org-theme-names',
array(
'methods' => 'GET',
'callback' => array( 'WP_Theme_Directory', 'get_theme_names' ),
'permission_callback' => function () {
return current_user_can( 'edit_theme_options' );
},
)
);
}
public static function get_theme_names() {
$html = wp_remote_get( self::THEME_NAMES_ENDPOINT );
// parse the html response extracting all the a inside li elements
$pattern = '/<li><a href=".*?">(.*?)<\/a><\/li>/';
preg_match_all( $pattern, $html['body'], $matches );
// Revemo the / from the end of the theme name
$cleaned_names = array_map(
function ( $name ) {
return str_replace( '/', '', $name );
},
$matches[1]
);
$names = array( 'names' => $cleaned_names );
return rest_ensure_response( $names );
}
function assets_enqueue() {
$asset_file = include( plugin_dir_path( dirname( __FILE__ ) ) . 'build/wp-org-theme-directory.asset.php' );
wp_register_script(
'wp-org-theme-directory',
plugins_url( 'build/wp-org-theme-directory.js', dirname( __FILE__ ) ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_enqueue_script(
'wp-org-theme-directory',
);
// Initialize and empty array of theme names to be shared between different client side scripts
wp_localize_script(
'wp-org-theme-directory',
'wpOrgThemeDirectory',
array(
'themeSlugs' => null,
)
);
}
}

View file

@ -7,14 +7,7 @@
"wideSize": "1000px"
},
"spacing": {
"units": [
"%",
"px",
"em",
"rem",
"vh",
"vw"
]
"units": [ "%", "px", "em", "rem", "vh", "vw" ]
},
"typography": {
"fontFamilies": [
@ -24,7 +17,8 @@
"slug": "system-font"
}
]
}
},
"useRootPaddingAwareAlignments": true
},
"templateParts": [
{

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@
* Plugin Name: Create Block Theme
* Plugin URI: https://wordpress.org/plugins/create-block-theme
* Description: Generates a block theme
* Version: 1.7.1
* Version: 1.8.2
* Author: WordPress.org
* Author URI: https://wordpress.org/
* License: GNU General Public License v2 or later

View file

@ -40,8 +40,9 @@ class Create_Block_Theme {
/**
* The class responsible for defining all actions that occur in the admin area.
*/
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-create-block-theme-admin.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-create-theme.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-manage-fonts.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/wp-org-theme-directory.php';
$this->loader = new Create_Block_Theme_Loader();
@ -58,6 +59,7 @@ class Create_Block_Theme {
$plugin_admin = new Create_Block_Theme_Admin();
$manage_fonts_admin = new Manage_Fonts_Admin();
$wp_theme_directory = new WP_Theme_Directory();
}
/**

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "create-block-theme",
"version": "1.7.1",
"version": "1.8.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "create-block-theme",
"version": "1.7.1",
"version": "1.8.2",
"license": "GPL-2.0-or-later",
"dependencies": {
"@wordpress/icons": "^9.17.0",

View file

@ -1,6 +1,6 @@
{
"name": "create-block-theme",
"version": "1.7.1",
"version": "1.8.2",
"private": true,
"description": "Create a block-based theme",
"author": "The WordPress Contributors",
@ -38,7 +38,7 @@
"simple-git": "^3.14.1"
},
"scripts": {
"build": "wp-scripts build",
"build": "wp-scripts build src/index.js src/plugin-sidebar.js src/wp-org-theme-directory.js",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:css:fix": "npm run lint:css -- --fix",
@ -47,7 +47,7 @@
"lint:php": "composer run-script lint",
"lint:php:fix": "composer run-script format",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start",
"start": "wp-scripts start src/index.js src/plugin-sidebar.js src/wp-org-theme-directory.js",
"update-version": "node update-version-and-changelog.js",
"prepare": "husky install"
},

View file

@ -56,6 +56,7 @@
</rule>
<!-- Directories and third party library exclusions. -->
<exclude-pattern>/build/*</exclude-pattern>
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>

View file

@ -3,8 +3,8 @@ Contributors: wordpressdotorg, mikachan, onemaggie, pbking, scruffian, mmaattiia
Donate link: https://automattic.com/
Tags: themes, theme, block-theme
Requires at least: 6.0
Tested up to: 6.1
Stable tag: 1.7.1
Tested up to: 6.2
Stable tag: 1.8.2
Requires PHP: 7.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@ -36,6 +36,41 @@ Still in the WordPress dashboard, navigate to "Appearance" -> "Create Block Them
== Changelog ==
= 1.8.2 =
* Merge branch 'trunk' into update/tested-up-wp-version-62
* Updating Tested up to: 6.2 WordPress version
* fix tag duplication in exported theme
* Fixing error checking
* Update Google Fonts JSON data from API
* Refactor react app code for general purpose
* add build directory to php exclude list
* Do not call replace_template_namespace when overwrting theme
* Merge branch 'trunk' into fix/314
* Fix error when switching to template edit mode in the post editor
* Add useRootPaddingAwareAlignments to blank theme
* Update Google Fonts JSON data from API
* Merge branch 'trunk' into fix/314
* Avoid adding Template info to style.css if it's empty
* Fix delete font family/face when name is different from family
* Add theme name validation
* Fix export theme from Site Editor
* Strip escaping characters before printing stylesheet
* Linting unlinted file
= 1.8.1 =
* Add current WordPress version to style.css and readme.txt
* Add labels around Google font family checkbox controls
* Fix theme slug, textdomain, and template for cloned, child and sibling themes.
* Replace theme slug in templates after getting media urls from them
= 1.8.0 =
* Export style variations just with the changes made by the user
* fix issue where package-lock is not updated on version bump
* Adding default value to an to avoid error when calling export_theme_data()
* Fixing image downloading not working in some cases
* Update Google Fonts JSON data from API
* Add Export (Clone) to site editor
= 1.7.1 =
* Update screenshots
* Fix manage fonts UI and backend when no settings are defined in theme.json

View file

@ -32,21 +32,31 @@ function FontVariant( { font, variant, isSelected, handleToggle } ) {
} );
}, [ font, variant ] );
const formattedFontFamily = font.family.toLowerCase().replace( ' ', '-' );
const fontId = `${ formattedFontFamily }-${ variant }`;
return (
<tr>
<td className="">
<input
type="checkbox"
name="google-font-variant"
id={ fontId }
value={ variant }
checked={ isSelected }
onClick={ handleToggle }
/>
</td>
<td className="">{ weight }</td>
<td className="">{ style }</td>
<td className="">
<label htmlFor={ fontId }>{ weight }</label>
</td>
<td className="">
<label htmlFor={ fontId }>{ style }</label>
</td>
<td className="demo-cell">
<label htmlFor={ fontId }>
<Demo style={ previewStyles } />
</label>
</td>
</tr>
);

View file

@ -182,6 +182,13 @@ function GoogleFonts() {
getFontCredits( googleFontsData.items[ value ] );
};
let selectedFontFamilyId = '';
if ( selectedFont ) {
selectedFontFamilyId = selectedFont.family
.toLowerCase()
.replace( ' ', '-' );
}
return (
<FontsPageLayout>
<main>
@ -261,6 +268,7 @@ function GoogleFonts() {
<td className="">
<input
type="checkbox"
id={ `select-all-${ selectedFontFamilyId }` }
onClick={ () =>
handleToggleAllVariants(
selectedFont.family
@ -276,22 +284,34 @@ function GoogleFonts() {
/>
</td>
<td className="">
<label
htmlFor={ `select-all-${ selectedFontFamilyId }` }
>
{ __(
'Weight',
'create-block-theme'
) }
</label>
</td>
<td className="">
<label
htmlFor={ `select-all-${ selectedFontFamilyId }` }
>
{ __(
'Style',
'create-block-theme'
) }
</label>
</td>
<td className="">
<label
htmlFor={ `select-all-${ selectedFontFamilyId }` }
>
{ __(
'Preview',
'create-block-theme'
) }
</label>
</td>
</tr>
</thead>

View file

@ -34,7 +34,7 @@ function App() {
window.addEventListener(
'load',
function () {
render( <App />, document.querySelector( '#fonts-app' ) );
render( <App />, document.querySelector( '#create-block-theme-app' ) );
},
false
);

View file

@ -84,7 +84,10 @@ function ManageFonts() {
function deleteFontFamily( fontFamily ) {
const updatedFonts = newThemeFonts.map( ( family ) => {
if ( fontFamily === family.fontFamily ) {
if (
fontFamily === family.fontFamily ||
fontFamily === family.name
) {
return {
...family,
shouldBeRemoved: true,
@ -110,7 +113,8 @@ function ManageFonts() {
if (
weight === face.fontWeight &&
style === face.fontStyle &&
fontFamily === family.fontFamily
( fontFamily === family.fontFamily ||
fontFamily === family.name )
) {
return {
...face,

183
src/plugin-sidebar.js Normal file
View file

@ -0,0 +1,183 @@
import { registerPlugin } from '@wordpress/plugins';
import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/edit-site';
import { tool } from '@wordpress/icons';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
Button,
// eslint-disable-next-line
__experimentalVStack as VStack,
// eslint-disable-next-line
__experimentalSpacer as Spacer,
// eslint-disable-next-line
__experimentalText as Text,
// eslint-disable-next-line
__experimentalHeading as Heading,
PanelBody,
TextControl,
} from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch, useSelect } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import { downloadFile } from './utils';
const ExportTheme = () => {
const { createErrorNotice } = useDispatch( noticesStore );
const [ theme, setTheme ] = useState( {
name: '',
description: '',
uri: '',
author: '',
author_uri: '',
tags_custom: '',
} );
useSelect( ( select ) => {
const themeData = select( 'core' ).getCurrentTheme();
setTheme( {
name: themeData.name.raw,
description: themeData.description.raw,
author: themeData.author.raw,
author_uri: themeData.author_uri.raw,
theme_uri: themeData.theme_uri.raw,
} );
}, [] );
const handleSubmit = () => {
const fetchOptions = {
path: '/create-block-theme/v1/export',
method: 'POST',
data: theme,
headers: {
'Content-Type': 'application/json',
},
parse: false,
};
async function exportTheme() {
try {
const response = await apiFetch( fetchOptions );
downloadFile( response );
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __(
'An error occurred while attempting to export the theme.'
);
createErrorNotice( errorMessage, { type: 'snackbar' } );
}
}
exportTheme();
};
return (
<PanelBody>
<Heading>{ __( 'Export', 'create-block-theme' ) }</Heading>
<VStack>
<Text variant="muted">
{ __(
'Export your theme with updated templates and styles.',
'create-block-theme'
) }
</Text>
<Spacer />
<TextControl
label={ __( 'Theme name', 'create-block-theme' ) }
value={ theme.name }
onChange={ ( value ) =>
setTheme( { ...theme, name: value } )
}
placeholder={ __( 'Theme name', 'create-block-theme' ) }
/>
<TextControl
label={ __( 'Theme description', 'create-block-theme' ) }
value={ theme.description }
onChange={ ( value ) =>
setTheme( { ...theme, description: value } )
}
placeholder={ __(
'A short description of the theme',
'create-block-theme'
) }
/>
<TextControl
label={ __( 'Theme URI', 'create-block-theme' ) }
value={ theme.uri }
onChange={ ( value ) =>
setTheme( { ...theme, uri: value } )
}
placeholder={ __(
'https://github.com/wordpress/twentytwentythree/',
'create-block-theme'
) }
/>
<TextControl
label={ __( 'Author', 'create-block-theme' ) }
value={ theme.author }
onChange={ ( value ) =>
setTheme( { ...theme, author: value } )
}
placeholder={ __(
'the WordPress team',
'create-block-theme'
) }
/>
<TextControl
label={ __( 'Author URI', 'create-block-theme' ) }
value={ theme.author_uri }
onChange={ ( value ) =>
setTheme( { ...theme, author_uri: value } )
}
placeholder={ __(
'https://wordpress.org/',
'create-block-theme'
) }
/>
</VStack>
<Spacer />
<Button
variant="secondary"
disabled={ ! theme.name }
onClick={ handleSubmit }
>
{ __( 'Export', 'create-block-theme' ) }
</Button>
<Spacer />
{ ! theme.name && (
<Text variant="muted">
{ __(
'Theme name is required for export.',
'create-block-theme'
) }
</Text>
) }
</PanelBody>
);
};
const CreateBlockThemePlugin = () => {
return (
<>
<PluginSidebarMoreMenuItem
target="create-block-theme-sidebar"
icon={ tool }
>
{ __( 'Create Block Theme' ) }
</PluginSidebarMoreMenuItem>
<PluginSidebar
name="create-block-theme-sidebar"
icon={ tool }
title={ __( 'Create Block Theme' ) }
>
<ExportTheme />
</PluginSidebar>
</>
);
};
registerPlugin( 'cbt-plugin-sidebar', {
render: CreateBlockThemePlugin,
} );

View file

@ -39,3 +39,30 @@ export function localFileAsThemeAssetUrl( url ) {
}
return url.replace( 'file:./', createBlockTheme.themeUrl + '/' );
}
export async function downloadFile( response ) {
const blob = await response.blob();
const filename = response.headers
.get( 'Content-Disposition' )
.split( 'filename=' )[ 1 ];
// Check if the browser supports navigator.msSaveBlob or navigator.saveBlob
if ( navigator.msSaveBlob || navigator.saveBlob ) {
const saveBlob = navigator.msSaveBlob || navigator.saveBlob;
saveBlob.call( navigator, blob, filename );
} else {
// Fall back to creating an object URL and triggering a download using an anchor element
const url = URL.createObjectURL( blob );
const a = document.createElement( 'a' );
a.href = url;
a.download = filename;
document.body.appendChild( a );
a.click();
document.body.removeChild( a );
setTimeout( () => {
URL.revokeObjectURL( url );
}, 100 );
}
}

View file

@ -0,0 +1,11 @@
import apiFetch from '@wordpress/api-fetch';
async function loadUnavailableThemeNames() {
const requestOptions = {
path: '/create-block-theme/v1/wp-org-theme-names',
};
const request = await apiFetch( requestOptions );
wpOrgThemeDirectory.themeSlugs = request.names;
}
window.addEventListener( 'load', loadUnavailableThemeNames );