]*>)(.*?)(<\/a>)/' );
104 | case 'core/quote':
105 | case 'core/pullquote':
106 | return array(
107 | '/(]*>)(.*?)(<\/p>)/',
108 | '/(]*>)(.*?)(<\/cite>)/',
109 | );
110 | case 'core/table':
111 | return array(
112 | '/(]*>)(.*?)(<\/td>)/',
113 | '/( ]*>)(.*?)(<\/th>)/',
114 | '/(]*>)(.*?)(<\/figcaption>)/',
115 | );
116 | case 'core/video':
117 | return array( '/(]*>)(.*?)(<\/figcaption>)/' );
118 | case 'core/image':
119 | return array(
120 | '/(]*>)(.*?)(<\/figcaption>)/',
121 | '/(alt=")(.*?)(")/',
122 | );
123 | case 'core/cover':
124 | case 'core/media-text':
125 | return array( '/(alt=")(.*?)(")/' );
126 | default:
127 | return null;
128 | }
129 | }
130 |
131 | /*
132 | * Localize text in text blocks.
133 | *
134 | * @param array $blocks The blocks to localize.
135 | * @return array The localized blocks.
136 | */
137 | public static function escape_text_content_of_blocks( $blocks ) {
138 | foreach ( $blocks as &$block ) {
139 |
140 | // Recursively escape the inner blocks.
141 | if ( ! empty( $block['innerBlocks'] ) ) {
142 | $block['innerBlocks'] = self::escape_text_content_of_blocks( $block['innerBlocks'] );
143 | }
144 |
145 | /*
146 | * Set the pattern based on the block type.
147 | * The pattern is used to match the content that needs to be escaped.
148 | * Patterns are defined in the get_text_replacement_patterns_for_html method.
149 | */
150 | $patterns = self::get_text_replacement_patterns_for_html( $block['blockName'] );
151 |
152 | // If the block does not have any patterns leave the block as is and continue to the next block.
153 | if ( ! $patterns ) {
154 | continue;
155 | }
156 |
157 | // Builds the replacement callback function based on the block type.
158 | switch ( $block['blockName'] ) {
159 | case 'core/paragraph':
160 | case 'core/heading':
161 | case 'core/list-item':
162 | case 'core/verse':
163 | case 'core/button':
164 | case 'core/quote':
165 | case 'core/pullquote':
166 | case 'core/table':
167 | case 'core/video':
168 | case 'core/image':
169 | case 'core/cover':
170 | case 'core/media-text':
171 | $replace_content_callback = function ( $content, $pattern ) {
172 | if ( empty( $content ) ) {
173 | return;
174 | }
175 | return preg_replace_callback(
176 | $pattern,
177 | function( $matches ) {
178 | // If the pattern is for attribute like alt="".
179 | if ( str_ends_with( $matches[1], '="' ) ) {
180 | return $matches[1] . self::escape_attribute( $matches[2] ) . $matches[3];
181 | }
182 | return $matches[1] . self::escape_text_content( $matches[2] ) . $matches[3];
183 | },
184 | $content
185 | );
186 | };
187 | break;
188 | default:
189 | $replace_content_callback = null;
190 | break;
191 | }
192 |
193 | // Apply the replacement patterns to the block content.
194 | foreach ( $patterns as $pattern ) {
195 | if (
196 | ! empty( $block['innerContent'] ) &&
197 | is_callable( $replace_content_callback )
198 | ) {
199 | $block['innerContent'] = is_array( $block['innerContent'] )
200 | ? array_map(
201 | function( $content ) use ( $replace_content_callback, $pattern ) {
202 | return $replace_content_callback( $content, $pattern );
203 | },
204 | $block['innerContent']
205 | )
206 | : $replace_content_callback( $block['innerContent'], $pattern );
207 | }
208 | }
209 | }
210 | return $blocks;
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/includes/create-theme/theme-media.php:
--------------------------------------------------------------------------------
1 | content );
28 | $blocks = _flatten_blocks( $template_blocks );
29 |
30 | $media = array();
31 |
32 | foreach ( $blocks as $block ) {
33 | // Gets the absolute URLs of img in these blocks
34 | if ( 'core/image' === $block['blockName'] ||
35 | 'core/video' === $block['blockName'] ||
36 | 'core/cover' === $block['blockName'] ||
37 | 'core/media-text' === $block['blockName']
38 | ) {
39 | $html = new WP_HTML_Tag_Processor( $block['innerHTML'] );
40 | while ( $html->next_tag( 'img' ) ) {
41 | $url = $html->get_attribute( 'src' );
42 | if ( CBT_Theme_Utils::is_absolute_url( $url ) ) {
43 | $media[] = $url;
44 | }
45 | }
46 | $html = new WP_HTML_Tag_Processor( $html->__toString() );
47 | while ( $html->next_tag( 'video' ) ) {
48 | $url = $html->get_attribute( 'src' );
49 | if ( CBT_Theme_Utils::is_absolute_url( $url ) ) {
50 | $media[] = $url;
51 | }
52 | $poster_url = $html->get_attribute( 'poster' );
53 | if ( CBT_Theme_Utils::is_absolute_url( $poster_url ) ) {
54 | $media[] = $poster_url;
55 | }
56 | }
57 | }
58 |
59 | // Gets the absolute URLs of background images in these blocks
60 | if ( 'core/cover' === $block['blockName'] ) {
61 | $html = new WP_HTML_Tag_Processor( $block['innerHTML'] );
62 | while ( $html->next_tag( 'div' ) ) {
63 | $style = $html->get_attribute( 'style' );
64 | if ( $style ) {
65 | $matches = array();
66 | preg_match( '/background-image: url\((.*)\)/', $style, $matches );
67 | if ( isset( $matches[1] ) ) {
68 | $url = $matches[1];
69 | if ( CBT_Theme_Utils::is_absolute_url( $url ) ) {
70 | $media[] = $url;
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | // Gets the absolute URLs of background images in these blocks
78 | if ( 'core/group' === $block['blockName'] ) {
79 | if ( isset( $block['attrs']['style']['background']['backgroundImage']['url'] ) && CBT_Theme_Utils::is_absolute_url( $block['attrs']['style']['background']['backgroundImage']['url'] ) ) {
80 | $media[] = $block['attrs']['style']['background']['backgroundImage']['url'];
81 | }
82 | }
83 | }
84 |
85 | return $media;
86 | }
87 |
88 | /**
89 | * Create a relative URL based on the absolute URL of a media file
90 | *
91 | * @param string $absolute_url
92 | * @return string $relative_url
93 | */
94 | public static function make_relative_media_url( $absolute_url ) {
95 | if ( ! empty( $absolute_url ) && CBT_Theme_Utils::is_absolute_url( $absolute_url ) ) {
96 | $folder_path = self::get_media_folder_path_from_url( $absolute_url );
97 | if ( is_child_theme() ) {
98 | return '' . $folder_path . basename( $absolute_url );
99 | }
100 | return '' . $folder_path . basename( $absolute_url );
101 | }
102 | return $absolute_url;
103 | }
104 |
105 | /**
106 | * Add media files to the local theme
107 | */
108 | public static function add_media_to_local( $media ) {
109 |
110 | foreach ( $media as $url ) {
111 |
112 | $download_file = download_url( $url );
113 |
114 | if ( is_wp_error( $download_file ) ) {
115 | //we're going to try again with a new URL
116 | //see, we might be running this in a docker container
117 | //and if that's the case let's try again on port 80
118 | $parsed_url = parse_url( $url );
119 | if ( 'localhost' === $parsed_url['host'] && '80' !== $parsed_url['port'] ) {
120 | $download_file = download_url( str_replace( 'localhost:' . $parsed_url['port'], 'localhost:80', $url ) );
121 | }
122 | }
123 |
124 | // TODO: implement a warning if the file is missing
125 | if ( ! is_wp_error( $download_file ) ) {
126 | $media_path = get_stylesheet_directory() . DIRECTORY_SEPARATOR . self::get_media_folder_path_from_url( $url );
127 | if ( ! is_dir( $media_path ) ) {
128 | wp_mkdir_p( $media_path );
129 | }
130 | rename( $download_file, $media_path . basename( $url ) );
131 | }
132 | }
133 |
134 | }
135 |
136 |
137 | /**
138 | * Replace the absolute URLs of media in a template with relative URLs
139 | */
140 | public static function make_template_images_local( $template ) {
141 |
142 | $template->media = self::get_media_absolute_urls_from_template( $template );
143 |
144 | // Replace the absolute URLs with relative URLs in the templates
145 | foreach ( $template->media as $media_url ) {
146 | $local_media_url = CBT_Theme_Media::make_relative_media_url( $media_url );
147 | $template->content = str_replace( $media_url, $local_media_url, $template->content );
148 | }
149 |
150 | return $template;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/includes/create-theme/theme-patterns.php:
--------------------------------------------------------------------------------
1 | get( 'TextDomain' );
6 | $pattern_slug = $theme_slug . '/' . $template->slug;
7 | $pattern_content = <<slug}
11 | * Slug: {$pattern_slug}
12 | * Inserter: no
13 | */
14 | ?>
15 | {$template->content}
16 | PHP;
17 |
18 | return array(
19 | 'slug' => $pattern_slug,
20 | 'content' => $pattern_content,
21 | );
22 | }
23 |
24 | public static function pattern_from_wp_block( $pattern_post ) {
25 | $pattern = new stdClass();
26 | $pattern->id = $pattern_post->ID;
27 | $pattern->title = $pattern_post->post_title;
28 | $pattern->name = sanitize_title_with_dashes( $pattern_post->post_title );
29 | $pattern->slug = wp_get_theme()->get( 'TextDomain' ) . '/' . $pattern->name;
30 | $pattern_category_list = get_the_terms( $pattern->id, 'wp_pattern_category' );
31 | $pattern->categories = ! empty( $pattern_category_list ) ? join( ', ', wp_list_pluck( $pattern_category_list, 'name' ) ) : '';
32 | $pattern->sync_status = get_post_meta( $pattern->id, 'wp_pattern_sync_status', true );
33 | $pattern->content = <<title}
37 | * Slug: {$pattern->slug}
38 | * Categories: {$pattern->categories}
39 | */
40 | ?>
41 | {$pattern_post->post_content}
42 | PHP;
43 |
44 | return $pattern;
45 | }
46 |
47 | public static function escape_alt_for_pattern( $html ) {
48 | if ( empty( $html ) ) {
49 | return $html;
50 | }
51 | $html = new WP_HTML_Tag_Processor( $html );
52 | while ( $html->next_tag( 'img' ) ) {
53 | $alt_attribute = $html->get_attribute( 'alt' );
54 | if ( ! empty( $alt_attribute ) ) {
55 | $html->set_attribute( 'alt', self::escape_text_for_pattern( $alt_attribute ) );
56 | }
57 | }
58 | return $html->__toString();
59 | }
60 |
61 | public static function escape_text_for_pattern( $text ) {
62 | if ( $text && trim( $text ) !== '' ) {
63 | $escaped_text = addslashes( $text );
64 | return "get( 'Name' ) . "');?>";
65 | }
66 | }
67 |
68 | public static function create_pattern_link( $attributes ) {
69 | $block_attributes = array_filter( $attributes );
70 | $attributes_json = json_encode( $block_attributes, JSON_UNESCAPED_SLASHES );
71 | return '';
72 | }
73 |
74 | public static function replace_local_pattern_references( $pattern ) {
75 | // Find any references to pattern in templates
76 | $templates_to_update = array();
77 | $args = array(
78 | 'post_type' => array( 'wp_template', 'wp_template_part' ),
79 | 'posts_per_page' => -1,
80 | 's' => 'wp:block {"ref":' . $pattern->id . '}',
81 | );
82 | $find_pattern_refs = new WP_Query( $args );
83 | if ( $find_pattern_refs->have_posts() ) {
84 | foreach ( $find_pattern_refs->posts as $post ) {
85 | $slug = $post->post_name;
86 | array_push( $templates_to_update, $slug );
87 | }
88 | }
89 | $templates_to_update = array_unique( $templates_to_update );
90 |
91 | // Only update templates that reference the pattern
92 | CBT_Theme_Templates::add_templates_to_local( 'all', null, null, $options, $templates_to_update );
93 |
94 | // List all template and pattern files in the theme
95 | $base_dir = get_stylesheet_directory();
96 | $patterns = glob( $base_dir . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . '*.php' );
97 | $templates = glob( $base_dir . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . '*.html' );
98 | $template_parts = glob( $base_dir . DIRECTORY_SEPARATOR . 'template-parts' . DIRECTORY_SEPARATOR . '*.html' );
99 |
100 | // Replace references to the local patterns in the theme
101 | foreach ( array_merge( $patterns, $templates, $template_parts ) as $file ) {
102 | $file_content = file_get_contents( $file );
103 | $file_content = str_replace( 'wp:block {"ref":' . $pattern->id . '}', 'wp:pattern {"slug":"' . $pattern->slug . '"}', $file_content );
104 | file_put_contents( $file, $file_content );
105 | }
106 |
107 | CBT_Theme_Templates::clear_user_templates_customizations();
108 | CBT_Theme_Templates::clear_user_template_parts_customizations();
109 | }
110 |
111 | public static function prepare_pattern_for_export( $pattern, $options = null ) {
112 | if ( ! $options ) {
113 | $options = array(
114 | 'localizeText' => false,
115 | 'removeNavRefs' => true,
116 | 'localizeImages' => true,
117 | );
118 | }
119 |
120 | $pattern = CBT_Theme_Templates::eliminate_environment_specific_content( $pattern, $options );
121 |
122 | if ( array_key_exists( 'localizeText', $options ) && $options['localizeText'] ) {
123 | $pattern = CBT_Theme_Templates::escape_text_in_template( $pattern );
124 | }
125 |
126 | if ( array_key_exists( 'localizeImages', $options ) && $options['localizeImages'] ) {
127 | $pattern = CBT_Theme_Media::make_template_images_local( $pattern );
128 |
129 | // Write the media assets if there are any
130 | if ( $pattern->media ) {
131 | CBT_Theme_Media::add_media_to_local( $pattern->media );
132 | }
133 | }
134 |
135 | return $pattern;
136 | }
137 |
138 | /**
139 | * Copy the local patterns as well as any media to the theme filesystem.
140 | */
141 | public static function add_patterns_to_theme( $options = null ) {
142 | $base_dir = get_stylesheet_directory();
143 | $patterns_dir = $base_dir . DIRECTORY_SEPARATOR . 'patterns';
144 |
145 | $pattern_query = new WP_Query(
146 | array(
147 | 'post_type' => 'wp_block',
148 | 'posts_per_page' => -1,
149 | )
150 | );
151 |
152 | if ( $pattern_query->have_posts() ) {
153 | // If there is no patterns folder, create it.
154 | if ( ! is_dir( $patterns_dir ) ) {
155 | wp_mkdir_p( $patterns_dir );
156 | }
157 |
158 | foreach ( $pattern_query->posts as $pattern ) {
159 | $pattern = self::pattern_from_wp_block( $pattern );
160 | $pattern = self::prepare_pattern_for_export( $pattern, $options );
161 | $pattern_exists = false;
162 |
163 | // Check pattern is synced before adding to theme.
164 | if ( 'unsynced' !== $pattern->sync_status ) {
165 | // Check pattern name doesn't already exist before creating the file.
166 | $existing_patterns = glob( $patterns_dir . DIRECTORY_SEPARATOR . '*.php' );
167 | foreach ( $existing_patterns as $existing_pattern ) {
168 | if ( strpos( $existing_pattern, $pattern->name . '.php' ) !== false ) {
169 | $pattern_exists = true;
170 | }
171 | }
172 |
173 | if ( $pattern_exists ) {
174 | return new WP_Error(
175 | 'pattern_already_exists',
176 | sprintf(
177 | /* Translators: Pattern name. */
178 | __(
179 | 'A pattern with this name already exists: "%s".',
180 | 'create-block-theme'
181 | ),
182 | $pattern->name
183 | )
184 | );
185 | }
186 |
187 | // Create the pattern file.
188 | $pattern_file = $patterns_dir . $pattern->name . '.php';
189 | file_put_contents(
190 | $patterns_dir . DIRECTORY_SEPARATOR . $pattern->name . '.php',
191 | $pattern->content
192 | );
193 |
194 | self::replace_local_pattern_references( $pattern );
195 |
196 | // Remove it from the database to ensure that these patterns are loaded from the theme.
197 | wp_delete_post( $pattern->id, true );
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/includes/create-theme/theme-styles.php:
--------------------------------------------------------------------------------
1 | 'License',
16 | 'LicenseURI' => 'License URI',
17 | )
18 | );
19 |
20 | $current_theme = wp_get_theme();
21 | $css_contents = trim( substr( $style_css, strpos( $style_css, '*/' ) + 2 ) );
22 | $name = stripslashes( $theme['name'] );
23 | $description = stripslashes( $theme['description'] );
24 | $uri = $theme['uri'];
25 | $author = stripslashes( $theme['author'] );
26 | $author_uri = $theme['author_uri'];
27 | $wp_version = CBT_Theme_Utils::get_current_wordpress_version();
28 | $requires_wp = ( '' === $theme['requires_wp'] ) ? CBT_Theme_Utils::get_current_wordpress_version() : $theme['requires_wp'];
29 | $version = $theme['version'];
30 | $requires_php = $current_theme->get( 'RequiresPHP' );
31 | $text_domain = $theme['slug'];
32 | $template = $current_theme->get( 'Template' ) ? "\n" . 'Template: ' . $current_theme->get( 'Template' ) : '';
33 | $license = $style_data['License'] ? $style_data['License'] : 'GNU General Public License v2 or later';
34 | $license_uri = $style_data['LicenseURI'] ? $style_data['LicenseURI'] : 'http://www.gnu.org/licenses/gpl-2.0.html';
35 | $tags = CBT_Theme_Tags::theme_tags_list( $theme );
36 | $css_contents = $css_contents ? "\n\n" . $css_contents : '';
37 | $copyright = '';
38 | preg_match( '/^\s*\n((?s).*?)\*\/\s*$/m', $style_css, $matches );
39 | if ( isset( $matches[1] ) ) {
40 | $copyright = "\n" . $matches[1];
41 | }
42 |
43 | return "/*
44 | Theme Name: {$name}
45 | Theme URI: {$uri}
46 | Author: {$author}
47 | Author URI: {$author_uri}
48 | Description: {$description}
49 | Requires at least: {$requires_wp}
50 | Tested up to: {$wp_version}
51 | Requires PHP: {$requires_php}
52 | Version: {$version}
53 | License: {$license}
54 | License URI: {$license_uri}{$template}
55 | Text Domain: {$text_domain}
56 | Tags: {$tags}
57 | {$copyright}*/{$css_contents}
58 | ";
59 | }
60 |
61 | /**
62 | * Build a style.css file for CHILD/GRANDCHILD themes.
63 | */
64 | public static function build_style_css( $theme ) {
65 | $name = stripslashes( $theme['name'] );
66 | $description = stripslashes( $theme['description'] );
67 | $uri = $theme['uri'];
68 | $author = stripslashes( $theme['author'] );
69 | $author_uri = $theme['author_uri'];
70 | $requires_wp = ( '' === $theme['requires_wp'] ) ? CBT_Theme_Utils::get_current_wordpress_version() : $theme['requires_wp'];
71 | $wp_version = CBT_Theme_Utils::get_current_wordpress_version();
72 | $text_domain = sanitize_title( $name );
73 | if ( isset( $theme['template'] ) ) {
74 | $template = $theme['template'];
75 | }
76 | $version = '1.0';
77 | $tags = CBT_Theme_Tags::theme_tags_list( $theme );
78 |
79 | if ( isset( $theme['version'] ) ) {
80 | $version = $theme['version'];
81 | }
82 |
83 | $style_css = "/*
84 | Theme Name: {$name}
85 | Theme URI: {$uri}
86 | Author: {$author}
87 | Author URI: {$author_uri}
88 | Description: {$description}
89 | Requires at least: {$requires_wp}
90 | Tested up to: {$wp_version}
91 | Requires PHP: 5.7
92 | Version: {$version}
93 | License: GNU General Public License v2 or later
94 | License URI: http://www.gnu.org/licenses/gpl-2.0.html
95 | ";
96 |
97 | if ( ! empty( $template ) ) {
98 | $style_css .= "Template: {$template}\n";
99 | }
100 |
101 | $style_css .= "Text Domain: {$text_domain}
102 | Tags: {$tags}
103 | */
104 |
105 | ";
106 |
107 | return $style_css;
108 | }
109 |
110 | public static function clear_user_styles_customizations() {
111 | // Clear all values in the user theme.json
112 | $user_custom_post_type_id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
113 | $global_styles_controller = new WP_REST_Global_Styles_Controller();
114 | $update_request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' );
115 | $update_request->set_param( 'id', $user_custom_post_type_id );
116 | $update_request->set_param( 'settings', array() );
117 | $update_request->set_param( 'styles', array() );
118 | $updated_global_styles = $global_styles_controller->update_item( $update_request );
119 | delete_transient( 'global_styles' );
120 | delete_transient( 'global_styles_' . get_stylesheet() );
121 | delete_transient( 'gutenberg_global_styles' );
122 | delete_transient( 'gutenberg_global_styles_' . get_stylesheet() );
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/includes/create-theme/theme-tags.php:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 | ' . __( 'read more', 'create-block-theme' ) . ''
48 | );
49 | ?>
50 |
51 |
52 |
53 | $tags ) {
60 | self::theme_tags_category( $category, $tags );
61 | }
62 | }
63 | ?>
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | $pretty_tag ) : ?>
93 |
94 |
95 |
96 | get( 'Tags' );
139 | $default_tags = self::list_default_tags();
140 | $merged_tags = array_unique( array_merge( $default_tags, $active_theme_tags ) );
141 |
142 | return in_array( $tag, $merged_tags, true );
143 | }
144 |
145 | /**
146 | * Build checkbox input for given theme tag.
147 | *
148 | * @param string $category
149 | * @param string $tag
150 | * @param string $pretty_tag
151 | * @return void
152 | */
153 | protected static function tag_checkbox_input( $category, $tag, $pretty_tag ) {
154 | $class = '';
155 | $checked = '';
156 |
157 | if ( self::is_default_tag( $tag ) ) {
158 | $class = 'default-tag';
159 | }
160 |
161 | if ( self::is_active_theme_tag( $tag ) ) {
162 | $checked = ' checked';
163 | }
164 | ?>
165 |
166 | >
167 |
168 |
169 | p = new WP_HTML_Tag_Processor( $string );
19 | }
20 |
21 | /**
22 | * Processes the HTML tags in the string and updates tokens, text, and translators' note.
23 | *
24 | * @param $p The string to process.
25 | * @return void
26 | */
27 | public function process_tokens() {
28 | while ( $this->p->next_token() ) {
29 | $token_type = $this->p->get_token_type();
30 | $token_name = strtolower( $this->p->get_token_name() );
31 | $is_tag_closer = $this->p->is_tag_closer();
32 | $has_self_closer = $this->p->has_self_closing_flag();
33 |
34 | if ( '#tag' === $token_type ) {
35 | $this->increment++;
36 | $this->text .= '%' . $this->increment . '$s';
37 | $token_label = $this->increment . '.';
38 |
39 | if ( 1 !== $this->increment ) {
40 | $this->translators_note .= ', ';
41 | }
42 |
43 | if ( $is_tag_closer ) {
44 | $this->tokens[] = "{$token_name}>";
45 | $this->translators_note .= $token_label . " is the end of a '" . $token_name . "' HTML element";
46 | } else {
47 | $token = '<' . $token_name;
48 | $attributes = $this->p->get_attribute_names_with_prefix( '' );
49 |
50 | foreach ( $attributes as $attr_name ) {
51 | $attr_value = $this->p->get_attribute( $attr_name );
52 | $token .= $this->process_attribute( $attr_name, $attr_value );
53 | }
54 |
55 | $token .= '>';
56 | $this->tokens[] = $token;
57 |
58 | if ( $has_self_closer || 'br' === $token_name ) {
59 | $this->translators_note .= $token_label . " is a '" . $token_name . "' HTML element";
60 | } else {
61 | $this->translators_note .= $token_label . " is the start of a '" . $token_name . "' HTML element";
62 | }
63 | }
64 | } else {
65 | // Escape text content.
66 | $temp_text = $this->p->get_modifiable_text();
67 |
68 | // If the text contains a %, we need to escape it.
69 | if ( false !== strpos( $temp_text, '%' ) ) {
70 | $temp_text = str_replace( '%', '%%', $temp_text );
71 | }
72 |
73 | $this->text .= $temp_text;
74 | }
75 | }
76 |
77 | if ( ! empty( $this->tokens ) ) {
78 | $this->translators_note .= ' */ ';
79 | }
80 | }
81 |
82 | /**
83 | * Processes individual tag attributes and escapes where necessary.
84 | *
85 | * @param string $attr_name The name of the attribute.
86 | * @param string $attr_value The value of the attribute.
87 | * @return string The processed attribute.
88 | */
89 | private function process_attribute( $attr_name, $attr_value ) {
90 | $token_part = '';
91 | if ( empty( $attr_value ) ) {
92 | $token_part .= ' ' . $attr_name;
93 | } elseif ( 'src' === $attr_name ) {
94 | CBT_Theme_Media::add_media_to_local( array( $attr_value ) );
95 | $relative_src = CBT_Theme_Media::get_media_folder_path_from_url( $attr_value ) . basename( $attr_value );
96 | $attr_value = "' . esc_url( get_stylesheet_directory_uri() ) . '{$relative_src}";
97 | $token_part .= ' ' . $attr_name . '="' . $attr_value . '"';
98 | } elseif ( 'href' === $attr_name ) {
99 | $attr_value = "' . esc_url( '$attr_value' ) . '";
100 | $token_part .= ' ' . $attr_name . '="' . $attr_value . '"';
101 | } else {
102 | $token_part .= ' ' . $attr_name . '="' . $attr_value . '"';
103 | }
104 |
105 | return $token_part;
106 | }
107 |
108 | /**
109 | * Gets the processed text.
110 | *
111 | * @return string
112 | */
113 | public function get_text() {
114 | return $this->text;
115 | }
116 |
117 | /**
118 | * Gets the processed tokens.
119 | *
120 | * @return array
121 | */
122 | public function get_tokens() {
123 | return $this->tokens;
124 | }
125 |
126 | /**
127 | * Gets the generated translators' note.
128 | *
129 | * @return string
130 | */
131 | public function get_translators_note() {
132 | return $this->translators_note;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/includes/create-theme/theme-utils.php:
--------------------------------------------------------------------------------
1 | get( 'TextDomain' );
43 | $old_name = $theme->get( 'Name' );
44 |
45 | // Get real path for our folder
46 | $theme_path = get_stylesheet_directory();
47 |
48 | // Create recursive directory iterator
49 | /** @var SplFileInfo[] $files */
50 | $files = new \RecursiveIteratorIterator(
51 | new \RecursiveDirectoryIterator( $theme_path, \RecursiveDirectoryIterator::SKIP_DOTS ),
52 | \RecursiveIteratorIterator::SELF_FIRST
53 | );
54 |
55 | // Add all the files (except for templates)
56 | foreach ( $files as $name => $file ) {
57 |
58 | // Get real and relative path for current file
59 | $file_path = wp_normalize_path( $file );
60 | $relative_path = substr( $file_path, strlen( $theme_path ) + 1 );
61 |
62 | // Create Directories
63 | if ( $file->isDir() ) {
64 | wp_mkdir_p( $location . DIRECTORY_SEPARATOR . $files->getSubPathname() );
65 | }
66 |
67 | // If the path is for templates/parts ignore it
68 | if (
69 | strpos( $file_path, 'block-template-parts/' ) ||
70 | strpos( $file_path, 'block-templates/' ) ||
71 | strpos( $file_path, 'templates/' ) ||
72 | strpos( $file_path, 'parts/' )
73 | ) {
74 | continue;
75 | }
76 |
77 | // Replace only text files, skip png's and other stuff.
78 | $contents = file_get_contents( $file_path );
79 | $valid_extensions = array( 'php', 'css', 'scss', 'js', 'txt', 'html' );
80 | $valid_extensions_regex = implode( '|', $valid_extensions );
81 |
82 | if ( preg_match( "/\.({$valid_extensions_regex})$/", $relative_path ) ) {
83 | // Replace namespace values if provided
84 | if ( $new_slug ) {
85 | $contents = self::replace_namespace( $contents, $old_slug, $new_slug, $old_name, $new_name );
86 | }
87 | }
88 |
89 | // Add current file to target
90 | file_put_contents( $location . DIRECTORY_SEPARATOR . $relative_path, $contents );
91 | }
92 | }
93 |
94 | public static function is_valid_screenshot( $file ) {
95 |
96 | $allowed_screenshot_types = array(
97 | 'png' => 'image/png',
98 | );
99 | $filetype = wp_check_filetype( $file['name'], $allowed_screenshot_types );
100 | if ( is_uploaded_file( $file['tmp_name'] ) && in_array( $filetype['type'], $allowed_screenshot_types, true ) && $file['size'] < 2097152 ) {
101 | return 1;
102 | }
103 | return 0;
104 | }
105 |
106 | public static function is_valid_screenshot_file( $file_path ) {
107 | return CBT_Theme_Utils::get_screenshot_file_extension( $file_path ) !== null;
108 | }
109 |
110 | public static function get_screenshot_file_extension( $file_path ) {
111 | $allowed_screenshot_types = array(
112 | 'png' => 'image/png',
113 | 'gif' => 'image/gif',
114 | 'jpg' => 'image/jpeg',
115 | 'jpeg' => 'image/jpeg',
116 | 'webp' => 'image/webp',
117 | 'avif' => 'image/avif',
118 | );
119 | $filetype = wp_check_filetype( $file_path, $allowed_screenshot_types );
120 | if ( in_array( $filetype['type'], $allowed_screenshot_types, true ) ) {
121 | return $filetype['ext'];
122 | }
123 | return null;
124 | }
125 |
126 | public static function copy_screenshot( $file_path ) {
127 |
128 | $new_screeenshot_id = attachment_url_to_postid( $file_path );
129 |
130 | if ( ! $new_screeenshot_id ) {
131 | return new \WP_Error( 'screenshot_not_found', __( 'Screenshot not found', 'create-block-theme' ) );
132 | }
133 |
134 | $new_screenshot_metadata = wp_get_attachment_metadata( $new_screeenshot_id );
135 | $upload_dir = wp_get_upload_dir();
136 |
137 | $new_screenshot_location = path_join( $upload_dir['basedir'], $new_screenshot_metadata['file'] );
138 |
139 | $new_screenshot_filetype = CBT_Theme_Utils::get_screenshot_file_extension( $file_path );
140 | $new_location = path_join( get_stylesheet_directory(), 'screenshot.' . $new_screenshot_filetype );
141 |
142 | // copy and resize the image
143 | $image_editor = wp_get_image_editor( $new_screenshot_location );
144 | $image_editor->resize( 1200, 900, true );
145 | $image_editor->save( $new_location );
146 |
147 | return true;
148 | }
149 |
150 | public static function replace_screenshot( $new_screenshot_path ) {
151 | if ( ! CBT_Theme_Utils::is_valid_screenshot_file( $new_screenshot_path ) ) {
152 | return new \WP_Error( 'invalid_screenshot', __( 'Invalid screenshot file', 'create-block-theme' ) );
153 | }
154 |
155 | // Remove the old screenshot
156 | $old_screenshot = wp_get_theme()->get_screenshot( 'relative' );
157 | if ( $old_screenshot ) {
158 | unlink( path_join( get_stylesheet_directory(), $old_screenshot ) );
159 | }
160 |
161 | // Copy the new screenshot
162 | return CBT_Theme_Utils::copy_screenshot( $new_screenshot_path );
163 | }
164 |
165 | /**
166 | * Get the current WordPress version.
167 | *
168 | * @return string The current WordPress in the format x.x (major.minor)
169 | * Example: 6.5
170 | */
171 | public static function get_current_wordpress_version() {
172 | $wp_version = get_bloginfo( 'version' );
173 | $wp_version_parts = explode( '.', $wp_version );
174 | return $wp_version_parts[0] . '.' . $wp_version_parts[1];
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/includes/index.php:
--------------------------------------------------------------------------------
1 | =20.10.0",
19 | "npm": ">=10.2.3"
20 | },
21 | "dependencies": {
22 | "@codemirror/lang-json": "^6.0.1",
23 | "@uiw/react-codemirror": "^4.23.1",
24 | "@wordpress/icons": "^10.7.0",
25 | "lib-font": "^2.4.3"
26 | },
27 | "devDependencies": {
28 | "@actions/core": "^1.10.0",
29 | "@emotion/babel-plugin": "^11.12.0",
30 | "@wordpress/base-styles": "^5.7.0",
31 | "@wordpress/browserslist-config": "^6.7.0",
32 | "@wordpress/env": "^10.14.0",
33 | "@wordpress/eslint-plugin": "^21.0.0",
34 | "@wordpress/prettier-config": "^4.7.0",
35 | "@wordpress/scripts": "^29.0.0",
36 | "@wordpress/stylelint-config": "^22.7.0",
37 | "babel-plugin-inline-json-import": "^0.3.2",
38 | "eslint-plugin-unicorn": "^55.0.0",
39 | "husky": "^9.1.5",
40 | "lint-staged": "^15.2.10",
41 | "prettier": "npm:wp-prettier@3.0.3",
42 | "simple-git": "^3.26.0"
43 | },
44 | "scripts": {
45 | "build": "wp-scripts build src/admin-landing-page.js src/plugin-sidebar.js",
46 | "format": "wp-scripts format",
47 | "lint:css": "wp-scripts lint-style",
48 | "lint:css:fix": "npm run lint:css -- --fix",
49 | "lint:js": "wp-scripts lint-js",
50 | "lint:js:fix": "npm run lint:js -- --fix",
51 | "lint:php": "composer run-script lint",
52 | "lint:php:fix": "composer run-script format",
53 | "lint:md-docs": "wp-scripts lint-md-docs",
54 | "lint:pkg-json": "wp-scripts lint-pkg-json",
55 | "test:php": "npm run test:php:setup && wp-env run tests-wordpress --env-cwd='wp-content/plugins/create-block-theme' composer run-script test",
56 | "test:php:watch": "wp-env run cli --env-cwd='wp-content/plugins/create-block-theme' composer run-script test:watch",
57 | "test:php:setup": "wp-env start",
58 | "packages-update": "wp-scripts packages-update",
59 | "start": "wp-scripts start src/admin-landing-page.js src/plugin-sidebar.js",
60 | "composer": "wp-env run cli --env-cwd=wp-content/plugins/create-block-theme composer",
61 | "update-version": "node update-version-and-changelog.js",
62 | "prepare": "husky install",
63 | "wp-env": "wp-env",
64 | "test:unit": "wp-scripts test-unit-js --config test/unit/jest.config.js"
65 | },
66 | "lint-staged": {
67 | "*.{js,json,yml}": [
68 | "wp-scripts format"
69 | ],
70 | "*.js": [
71 | "npm run lint:js"
72 | ],
73 | "*.{css,scss}": [
74 | "npm run lint:css"
75 | ],
76 | "*.php": [
77 | "npm run lint:php"
78 | ],
79 | "*.md": [
80 | "npm run lint:md-docs"
81 | ],
82 | "package.json": [
83 | "npm run lint:pkg-json"
84 | ]
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | Apply WordPress Coding Standards to all files
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | .
37 |
38 |
39 |
40 |
41 |
42 |
43 | warning
44 |
45 |
46 | warning
47 |
48 |
49 | warning
50 |
51 |
52 | warning
53 |
54 |
55 | warning
56 |
57 |
58 |
59 | /build/*
60 | /vendor/*
61 | /node_modules/*
62 |
63 |
64 |
65 | *
66 |
67 |
68 |
69 |
70 | *
71 |
72 |
73 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/
14 | ./tests/test-sample.php
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/admin-landing-page.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { createRoot } from '@wordpress/element';
5 |
6 | /**
7 | * Internal dependencies
8 | */
9 | import './admin-landing-page.scss';
10 | import LandingPage from './landing-page/landing-page';
11 |
12 | function App() {
13 | return ;
14 | }
15 |
16 | window.addEventListener(
17 | 'load',
18 | function () {
19 | const domNode = document.getElementById( 'create-block-theme-app' );
20 | const root = createRoot( domNode );
21 | root.render( );
22 | },
23 | false
24 | );
25 |
--------------------------------------------------------------------------------
/src/admin-landing-page.scss:
--------------------------------------------------------------------------------
1 | @import "../node_modules/@wordpress/base-styles/mixins";
2 | @include wordpress-admin-schemes();
3 |
4 |
5 | .create-block-theme {
6 | &__landing-page {
7 | background-color: #fff;
8 | margin-left: -20px;
9 | a,
10 | button {
11 | color: #3858e9;
12 | }
13 | &__header {
14 | width: 100%;
15 | background-color: #2d59f2;
16 | margin: 0;
17 | }
18 | &__body {
19 | padding: 40px 0;
20 | p {
21 | margin-top: 0;
22 | }
23 | h1,
24 | h2,
25 | h3,
26 | h4,
27 | h5,
28 | h6 {
29 | margin-top: 0.3em;
30 | margin-bottom: 0.3em;
31 | }
32 | h2 {
33 | font-size: 2em;
34 | }
35 | h3 {
36 | font-size: 1em;
37 | }
38 | @media screen and (max-width: 775px) {
39 | flex-direction: column;
40 | h2 {
41 | font-size: 1.5em;
42 | }
43 | }
44 | &__left-column {
45 | flex: 1;
46 | margin: 0 60px;
47 | button {
48 | font-size: 1.75em;
49 | @media screen and (max-width: 775px) {
50 | font-size: 1.25em;
51 | }
52 | }
53 | }
54 | &__right-column {
55 | max-width: 330px;
56 | margin: 0 60px;
57 | @media screen and (max-width: 775px) {
58 | max-width: 100%;
59 | }
60 | p {
61 | margin-bottom: 0;
62 | }
63 | }
64 |
65 | &__faq {
66 | img {
67 | max-width: 100%;
68 | }
69 | p {
70 | padding: 10px;
71 | font-style: italic;
72 | }
73 | details {
74 | padding-bottom: 20px;
75 |
76 | summary {
77 | cursor: pointer;
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/editor-sidebar/about.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { __ } from '@wordpress/i18n';
5 | import {
6 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
7 | __experimentalVStack as VStack,
8 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
9 | __experimentalText as Text,
10 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
11 | __experimentalDivider as Divider,
12 | PanelBody,
13 | ExternalLink,
14 | } from '@wordpress/components';
15 |
16 | /**
17 | * Internal dependencies
18 | */
19 | import ScreenHeader from './screen-header';
20 |
21 | function AboutPlugin() {
22 | return (
23 |
24 |
27 |
28 |
29 | { __(
30 | 'Create Block Theme is a tool to help you make Block Themes using the WordPress Editor. It does this by adding tools to the Editor to help you create and manage your theme.',
31 | 'create-block-theme'
32 | ) }
33 |
34 |
35 |
36 | { __(
37 | "Themes created with Create Block Theme don't require Create Block Theme to be installed on the site where the theme is used.",
38 | 'create-block-theme'
39 | ) }
40 |
41 |
42 |
43 |
44 |
45 | { __( 'Help', 'create-block-theme' ) }
46 |
47 |
48 |
49 | <>
50 | { __( 'Have a question?', 'create-block-theme' ) }
51 |
52 |
53 | { __( 'Ask in the forums.', 'create-block-theme' ) }
54 |
55 | >
56 |
57 |
58 |
59 | <>
60 | { __( 'Found a bug?', 'create-block-theme' ) }
61 |
62 |
63 | { __(
64 | 'Report it on GitHub.',
65 | 'create-block-theme'
66 | ) }
67 |
68 | >
69 |
70 |
71 |
72 | <>
73 | { __( 'Want to contribute?', 'create-block-theme' ) }
74 |
75 |
76 | { __(
77 | 'Check out the project on GitHub.',
78 | 'create-block-theme'
79 | ) }
80 |
81 | >
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | export default AboutPlugin;
89 |
--------------------------------------------------------------------------------
/src/editor-sidebar/create-panel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { __ } from '@wordpress/i18n';
5 | import { useState } from '@wordpress/element';
6 | import { useDispatch } from '@wordpress/data';
7 | import { store as noticesStore } from '@wordpress/notices';
8 | import {
9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
10 | __experimentalVStack as VStack,
11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
12 | __experimentalSpacer as Spacer,
13 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
14 | __experimentalText as Text,
15 | PanelBody,
16 | Button,
17 | SelectControl,
18 | TextControl,
19 | TextareaControl,
20 | } from '@wordpress/components';
21 | import { addCard, copy } from '@wordpress/icons';
22 |
23 | /**
24 | * Internal dependencies
25 | */
26 | import ScreenHeader from './screen-header';
27 | import {
28 | createBlankTheme,
29 | createClonedTheme,
30 | createChildTheme,
31 | } from '../resolvers';
32 | import { generateWpVersions } from '../utils/generate-versions';
33 |
34 | const WP_MINIMUM_VERSIONS = generateWpVersions( WP_VERSION ); // eslint-disable-line no-undef
35 |
36 | export const CreateThemePanel = ( { createType } ) => {
37 | const { createErrorNotice } = useDispatch( noticesStore );
38 |
39 | const [ theme, setTheme ] = useState( {
40 | name: '',
41 | description: '',
42 | uri: '',
43 | author: '',
44 | author_uri: '',
45 | tags_custom: '',
46 | requires_wp: '',
47 | } );
48 |
49 | const cloneTheme = () => {
50 | if ( createType === 'createClone' ) {
51 | handleCloneClick();
52 | } else if ( createType === 'createChild' ) {
53 | handleCreateChildClick();
54 | }
55 | };
56 |
57 | const handleCreateBlankClick = () => {
58 | createBlankTheme( theme )
59 | .then( () => {
60 | // eslint-disable-next-line no-alert
61 | window.alert(
62 | __(
63 | 'Theme created successfully. The editor will now reload.',
64 | 'create-block-theme'
65 | )
66 | );
67 | window.location.reload();
68 | } )
69 | .catch( ( error ) => {
70 | const errorMessage =
71 | error.message ||
72 | __(
73 | 'An error occurred while attempting to create the theme.',
74 | 'create-block-theme'
75 | );
76 | createErrorNotice( errorMessage, { type: 'snackbar' } );
77 | } );
78 | };
79 |
80 | const handleCloneClick = () => {
81 | createClonedTheme( theme )
82 | .then( () => {
83 | // eslint-disable-next-line no-alert
84 | window.alert(
85 | __(
86 | 'Theme cloned successfully. The editor will now reload.',
87 | 'create-block-theme'
88 | )
89 | );
90 | window.location.reload();
91 | } )
92 | .catch( ( error ) => {
93 | const errorMessage =
94 | error.message ||
95 | __(
96 | 'An error occurred while attempting to create the theme.',
97 | 'create-block-theme'
98 | );
99 | createErrorNotice( errorMessage, { type: 'snackbar' } );
100 | } );
101 | };
102 |
103 | const handleCreateChildClick = () => {
104 | createChildTheme( theme )
105 | .then( () => {
106 | // eslint-disable-next-line no-alert
107 | window.alert(
108 | __(
109 | 'Child theme created successfully. The editor will now reload.',
110 | 'create-block-theme'
111 | )
112 | );
113 | window.location.reload();
114 | } )
115 | .catch( ( error ) => {
116 | const errorMessage =
117 | error.message ||
118 | __(
119 | 'An error occurred while attempting to create the theme.',
120 | 'create-block-theme'
121 | );
122 | createErrorNotice( errorMessage, { type: 'snackbar' } );
123 | } );
124 | };
125 |
126 | return (
127 |
128 |
131 |
132 |
138 | setTheme( { ...theme, name: value } )
139 | }
140 | />
141 |
142 |
143 | { __(
144 | 'Additional Theme MetaData',
145 | 'create-block-theme'
146 | ) }
147 |
148 |
149 |
150 |
158 | setTheme( { ...theme, description: value } )
159 | }
160 | placeholder={ __(
161 | 'A short description of the theme',
162 | 'create-block-theme'
163 | ) }
164 | />
165 |
171 | setTheme( { ...theme, uri: value } )
172 | }
173 | placeholder={ __(
174 | 'https://github.com/wordpress/twentytwentythree/',
175 | 'create-block-theme'
176 | ) }
177 | />
178 |
184 | setTheme( { ...theme, author: value } )
185 | }
186 | placeholder={ __(
187 | 'the WordPress team',
188 | 'create-block-theme'
189 | ) }
190 | />
191 |
197 | setTheme( { ...theme, author_uri: value } )
198 | }
199 | placeholder={ __(
200 | 'https://wordpress.org/',
201 | 'create-block-theme'
202 | ) }
203 | />
204 | ( {
214 | label: version,
215 | value: version,
216 | } )
217 | ) }
218 | onChange={ ( value ) => {
219 | setTheme( { ...theme, requires_wp: value } );
220 | } }
221 | />
222 |
223 |
224 |
225 | { createType === 'createClone' && (
226 | <>
227 | cloneTheme() }
231 | >
232 | { __( 'Create Theme', 'create-block-theme' ) }
233 |
234 | >
235 | ) }
236 | { createType === 'createChild' && (
237 | <>
238 | cloneTheme() }
242 | >
243 | { __( 'Create Child Theme', 'create-block-theme' ) }
244 |
245 | >
246 | ) }
247 | { createType === 'createBlank' && (
248 | <>
249 |
254 | { __( 'Create Blank Theme', 'create-block-theme' ) }
255 |
256 |
257 |
258 | { __(
259 | 'Create a blank theme with no styles or templates.',
260 | 'create-block-theme'
261 | ) }
262 |
263 | >
264 | ) }
265 |
266 |
267 | );
268 | };
269 |
--------------------------------------------------------------------------------
/src/editor-sidebar/create-variation-panel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { __ } from '@wordpress/i18n';
5 | import { useState } from '@wordpress/element';
6 | import { useDispatch, useSelect } from '@wordpress/data';
7 | import { store as noticesStore } from '@wordpress/notices';
8 | import {
9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
10 | __experimentalVStack as VStack,
11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
12 | __experimentalText as Text,
13 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
14 | __experimentalSpacer as Spacer,
15 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
16 | __experimentalView as View,
17 | PanelBody,
18 | Button,
19 | TextControl,
20 | CheckboxControl,
21 | } from '@wordpress/components';
22 | import { copy } from '@wordpress/icons';
23 | import { store as preferencesStore } from '@wordpress/preferences';
24 |
25 | /**
26 | * Internal dependencies
27 | */
28 | import { postCreateThemeVariation } from '../resolvers';
29 | import ScreenHeader from './screen-header';
30 |
31 | const PREFERENCE_SCOPE = 'create-block-theme';
32 | const PREFERENCE_KEY = 'create-variation';
33 |
34 | export const CreateVariationPanel = () => {
35 | const { createErrorNotice } = useDispatch( noticesStore );
36 |
37 | const [ theme, setTheme ] = useState( {
38 | name: '',
39 | } );
40 |
41 | const preference = useSelect( ( select ) => {
42 | const _preference = select( preferencesStore ).get(
43 | PREFERENCE_SCOPE,
44 | PREFERENCE_KEY
45 | );
46 | return {
47 | saveFonts: _preference?.saveFonts ?? true,
48 | };
49 | }, [] );
50 |
51 | const handleTogglePreference = ( key ) => {
52 | setPreference( PREFERENCE_SCOPE, PREFERENCE_KEY, {
53 | ...preference,
54 | [ key ]: ! preference[ key ],
55 | } );
56 | };
57 |
58 | const { set: setPreference } = useDispatch( preferencesStore );
59 |
60 | const handleCreateVariationClick = () => {
61 | const variationPreferences = {
62 | name: theme.name,
63 | ...preference,
64 | };
65 |
66 | postCreateThemeVariation( variationPreferences )
67 | .then( () => {
68 | // eslint-disable-next-line no-alert
69 | window.alert(
70 | __(
71 | 'Theme variation created successfully. The editor will now reload.',
72 | 'create-block-theme'
73 | )
74 | );
75 | window.location.reload();
76 | } )
77 | .catch( ( error ) => {
78 | const errorMessage =
79 | error.message ||
80 | __(
81 | 'An error occurred while attempting to create the theme variation.',
82 | 'create-block-theme'
83 | );
84 | createErrorNotice( errorMessage, { type: 'snackbar' } );
85 | } );
86 | };
87 |
88 | return (
89 |
90 |
93 |
94 |
95 |
96 | { __(
97 | 'Save the Global Styles changes as a theme variation.',
98 | 'create-block-theme'
99 | ) }
100 |
101 |
102 |
103 |
104 |
105 |
114 | setTheme( { ...theme, name: value } )
115 | }
116 | />
117 |
118 |
130 | handleTogglePreference( 'saveFonts' )
131 | }
132 | />
133 |
134 |
139 | { __(
140 | 'Create Theme Variation',
141 | 'create-block-theme'
142 | ) }
143 |
144 |
145 |
146 |
147 |
148 |
149 | );
150 | };
151 |
--------------------------------------------------------------------------------
/src/editor-sidebar/global-styles-json-editor-modal.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | import CodeMirror from '@uiw/react-codemirror';
5 | import { json } from '@codemirror/lang-json';
6 |
7 | /**
8 | * WordPress dependencies
9 | */
10 | import { __, sprintf } from '@wordpress/i18n';
11 | import { Modal } from '@wordpress/components';
12 | import { useSelect } from '@wordpress/data';
13 | import { store as coreStore } from '@wordpress/core-data';
14 |
15 | const GlobalStylesJsonEditorModal = ( { onRequestClose } ) => {
16 | const themeName = useSelect( ( select ) =>
17 | select( 'core' ).getCurrentTheme()
18 | )?.name?.raw;
19 |
20 | const { record: globalStylesRecord } = useSelect( ( select ) => {
21 | const {
22 | __experimentalGetCurrentGlobalStylesId,
23 | getEditedEntityRecord,
24 | } = select( coreStore );
25 | const globalStylesId = __experimentalGetCurrentGlobalStylesId();
26 | const record = getEditedEntityRecord(
27 | 'root',
28 | 'globalStyles',
29 | globalStylesId
30 | );
31 | return {
32 | record,
33 | };
34 | } );
35 |
36 | const globalStyles = {
37 | ...( globalStylesRecord?.styles && {
38 | styles: globalStylesRecord.styles,
39 | } ),
40 | ...( globalStylesRecord?.settings && {
41 | settings: globalStylesRecord.settings,
42 | } ),
43 | };
44 |
45 | const globalStylesAsString = globalStyles
46 | ? JSON.stringify( globalStyles, null, 4 )
47 | : '';
48 |
49 | const handleSave = () => {};
50 |
51 | return (
52 |
62 |
68 |
69 | );
70 | };
71 |
72 | export default GlobalStylesJsonEditorModal;
73 |
--------------------------------------------------------------------------------
/src/editor-sidebar/json-editor-modal.js:
--------------------------------------------------------------------------------
1 | /**
2 | * External dependencies
3 | */
4 | import CodeMirror from '@uiw/react-codemirror';
5 | import { json } from '@codemirror/lang-json';
6 |
7 | /**
8 | * WordPress dependencies
9 | */
10 | import { __, sprintf } from '@wordpress/i18n';
11 | import { useState, useEffect } from '@wordpress/element';
12 | import { Modal } from '@wordpress/components';
13 | import { useSelect } from '@wordpress/data';
14 |
15 | /**
16 | * Internal dependencies
17 | */
18 | import { fetchThemeJson } from '../resolvers';
19 |
20 | const ThemeJsonEditorModal = ( { onRequestClose } ) => {
21 | const [ themeData, setThemeData ] = useState( '' );
22 | const themeName = useSelect( ( select ) =>
23 | select( 'core' ).getCurrentTheme()
24 | )?.name?.raw;
25 | const fetchThemeData = async () => {
26 | setThemeData( await fetchThemeJson() );
27 | };
28 | const handleSave = () => {};
29 |
30 | useEffect( () => {
31 | fetchThemeData();
32 | } );
33 |
34 | return (
35 |
45 |
51 |
52 | );
53 | };
54 |
55 | export default ThemeJsonEditorModal;
56 |
--------------------------------------------------------------------------------
/src/editor-sidebar/reset-theme.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { __ } from '@wordpress/i18n';
5 | import { useDispatch, useSelect } from '@wordpress/data';
6 | import { store as noticesStore } from '@wordpress/notices';
7 | import {
8 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
9 | __experimentalConfirmDialog as ConfirmDialog,
10 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
11 | __experimentalVStack as VStack,
12 | PanelBody,
13 | Button,
14 | CheckboxControl,
15 | } from '@wordpress/components';
16 | import { trash } from '@wordpress/icons';
17 | import { useState } from '@wordpress/element';
18 | import { store as preferencesStore } from '@wordpress/preferences';
19 |
20 | /**
21 | * Internal dependencies
22 | */
23 | import ScreenHeader from './screen-header';
24 | import { resetTheme } from '../resolvers';
25 |
26 | const PREFERENCE_SCOPE = 'create-block-theme';
27 | const PREFERENCE_KEY = 'reset-theme';
28 |
29 | function ResetTheme() {
30 | const preferences = useSelect( ( select ) => {
31 | const _preference = select( preferencesStore ).get(
32 | PREFERENCE_SCOPE,
33 | PREFERENCE_KEY
34 | );
35 | return {
36 | resetStyles: _preference?.resetStyles ?? true,
37 | resetTemplates: _preference?.resetTemplates ?? true,
38 | resetTemplateParts: _preference?.resetTemplateParts ?? true,
39 | };
40 | }, [] );
41 |
42 | const { set: setPreferences } = useDispatch( preferencesStore );
43 | const { createErrorNotice } = useDispatch( noticesStore );
44 | const [ isConfirmDialogOpen, setIsConfirmDialogOpen ] = useState( false );
45 |
46 | const handleTogglePreference = ( key ) => {
47 | setPreferences( PREFERENCE_SCOPE, PREFERENCE_KEY, {
48 | ...preferences,
49 | [ key ]: ! preferences[ key ],
50 | } );
51 | };
52 |
53 | const toggleConfirmDialog = () => {
54 | setIsConfirmDialogOpen( ! isConfirmDialogOpen );
55 | };
56 |
57 | const handleResetTheme = async () => {
58 | try {
59 | await resetTheme( preferences );
60 | toggleConfirmDialog();
61 | // eslint-disable-next-line no-alert
62 | window.alert(
63 | __(
64 | 'Theme reset successfully. The editor will now reload.',
65 | 'create-block-theme'
66 | )
67 | );
68 | window.location.reload();
69 | } catch ( error ) {
70 | createErrorNotice(
71 | __(
72 | 'An error occurred while resetting the theme.',
73 | 'create-block-theme'
74 | )
75 | );
76 | }
77 | };
78 |
79 | return (
80 | <>
81 |
88 | { __(
89 | 'Are you sure you want to reset the theme? This action cannot be undone.',
90 | 'create-block-theme'
91 | ) }
92 |
93 |
94 |
97 |
98 |
110 | handleTogglePreference( 'resetStyles' )
111 | }
112 | />
113 |
114 |
126 | handleTogglePreference( 'resetTemplates' )
127 | }
128 | />
129 |
130 |
142 | handleTogglePreference( 'resetTemplateParts' )
143 | }
144 | />
145 |
146 |
157 |
158 |
159 | >
160 | );
161 | }
162 |
163 | export default ResetTheme;
164 |
--------------------------------------------------------------------------------
/src/editor-sidebar/save-panel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { __ } from '@wordpress/i18n';
5 | import { useSelect, useDispatch } from '@wordpress/data';
6 | import { store as noticesStore } from '@wordpress/notices';
7 | import apiFetch from '@wordpress/api-fetch';
8 | import {
9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
10 | __experimentalVStack as VStack,
11 | PanelBody,
12 | Button,
13 | CheckboxControl,
14 | } from '@wordpress/components';
15 | import { archive } from '@wordpress/icons';
16 | import { store as preferencesStore } from '@wordpress/preferences';
17 |
18 | /**
19 | * Internal dependencies
20 | */
21 | import ScreenHeader from './screen-header';
22 |
23 | const PREFERENCE_SCOPE = 'create-block-theme';
24 | const PREFERENCE_KEY = 'save-changes';
25 |
26 | export const SaveThemePanel = () => {
27 | const preference = useSelect( ( select ) => {
28 | const _preference = select( preferencesStore ).get(
29 | PREFERENCE_SCOPE,
30 | PREFERENCE_KEY
31 | );
32 | return {
33 | saveStyle: _preference?.saveStyle ?? true,
34 | saveTemplates: _preference?.saveTemplates ?? true,
35 | processOnlySavedTemplates:
36 | _preference?.processOnlySavedTemplates ?? true,
37 | savePatterns: _preference?.savePatterns ?? true,
38 | saveFonts: _preference?.saveFonts ?? true,
39 | removeNavRefs: _preference?.removeNavRefs ?? false,
40 | localizeText: _preference?.localizeText ?? false,
41 | localizeImages: _preference?.localizeImages ?? false,
42 | removeTaxQuery: _preference?.removeTaxQuery ?? false,
43 | };
44 | }, [] );
45 |
46 | const { createErrorNotice } = useDispatch( noticesStore );
47 | const { set: setPreference } = useDispatch( preferencesStore );
48 |
49 | const handleTogglePreference = ( key ) => {
50 | setPreference( PREFERENCE_SCOPE, PREFERENCE_KEY, {
51 | ...preference,
52 | [ key ]: ! preference[ key ],
53 | } );
54 | };
55 |
56 | const handleSaveClick = () => {
57 | apiFetch( {
58 | path: '/create-block-theme/v1/save',
59 | method: 'POST',
60 | data: preference,
61 | headers: {
62 | 'Content-Type': 'application/json',
63 | },
64 | } )
65 | .then( () => {
66 | // eslint-disable-next-line no-alert
67 | window.alert(
68 | __(
69 | 'Theme saved successfully. The editor will now reload.',
70 | 'create-block-theme'
71 | )
72 | );
73 |
74 | const searchParams = new URLSearchParams(
75 | window?.location?.search
76 | );
77 | // If user is editing a pattern and savePatterns is true, redirect back to the patterns page.
78 | if (
79 | preference.savePatterns &&
80 | searchParams.get( 'postType' ) === 'wp_block' &&
81 | searchParams.get( 'postId' )
82 | ) {
83 | window.location =
84 | '/wp-admin/site-editor.php?postType=wp_block';
85 | } else {
86 | // If user is not editing a pattern, reload the editor.
87 | window.location.reload();
88 | }
89 | } )
90 | .catch( ( error ) => {
91 | const errorMessage =
92 | error.message ||
93 | __(
94 | 'An error occurred while attempting to save the theme.',
95 | 'create-block-theme'
96 | );
97 | createErrorNotice( errorMessage, { type: 'snackbar' } );
98 | } );
99 | };
100 |
101 | return (
102 |
103 |
106 |
107 | handleTogglePreference( 'saveFonts' ) }
116 | />
117 | handleTogglePreference( 'saveStyle' ) }
126 | />
127 | handleTogglePreference( 'saveTemplates' ) }
139 | />
140 |
156 | handleTogglePreference( 'processOnlySavedTemplates' )
157 | }
158 | />
159 | handleTogglePreference( 'savePatterns' ) }
168 | />
169 | handleTogglePreference( 'localizeText' ) }
185 | />
186 |
202 | handleTogglePreference( 'localizeImages' )
203 | }
204 | />
205 | handleTogglePreference( 'removeNavRefs' ) }
224 | />
225 |
240 | handleTogglePreference( 'removeTaxQuery' )
241 | }
242 | />
243 |
248 | { __( 'Save Changes', 'create-block-theme' ) }
249 |
250 |
251 |
252 | );
253 | };
254 |
--------------------------------------------------------------------------------
/src/editor-sidebar/screen-header.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import {
5 | Navigator,
6 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
7 | __experimentalHStack as HStack,
8 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
9 | __experimentalSpacer as Spacer,
10 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
11 | __experimentalHeading as Heading,
12 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
13 | __experimentalNavigatorToParentButton as NavigatorToParentButton,
14 | } from '@wordpress/components';
15 | import { isRTL, __ } from '@wordpress/i18n';
16 | import { chevronRight, chevronLeft } from '@wordpress/icons';
17 |
18 | const ScreenHeader = ( { title, onBack } ) => {
19 | // TODO: Remove the fallback component when the minimum supported WordPress
20 | // version was increased to 6.7.
21 | const BackButton = Navigator?.BackButton || NavigatorToParentButton;
22 | return (
23 |
24 |
25 |
32 |
33 |
39 | { title }
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default ScreenHeader;
48 |
--------------------------------------------------------------------------------
/src/landing-page/create-modal.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { __ } from '@wordpress/i18n';
5 | import { useState } from '@wordpress/element';
6 | import {
7 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
8 | __experimentalHStack as HStack,
9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
10 | __experimentalVStack as VStack,
11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
12 | __experimentalText as Text,
13 | Modal,
14 | Button,
15 | TextControl,
16 | TextareaControl,
17 | } from '@wordpress/components';
18 |
19 | /**
20 | * Internal dependencies
21 | */
22 | import {
23 | createBlankTheme,
24 | createClonedTheme,
25 | createChildTheme,
26 | } from '../resolvers';
27 |
28 | export const CreateThemeModal = ( { onRequestClose, creationType } ) => {
29 | const [ errorMessage, setErrorMessage ] = useState( null );
30 |
31 | const [ theme, setTheme ] = useState( {
32 | name: '',
33 | description: '',
34 | author: '',
35 | } );
36 |
37 | const renderCreateButtonText = ( type ) => {
38 | switch ( type ) {
39 | case 'blank':
40 | return __(
41 | 'Create and Activate Blank Theme',
42 | 'create-block-theme'
43 | );
44 | case 'clone':
45 | return __( 'Clone Block Theme', 'create-block-theme' );
46 | case 'child':
47 | return __( 'Create Child Theme', 'create-block-theme' );
48 | }
49 | };
50 |
51 | const createBlockTheme = async () => {
52 | let constructionFunction = null;
53 | switch ( creationType ) {
54 | case 'blank':
55 | constructionFunction = createBlankTheme;
56 | break;
57 | case 'clone':
58 | constructionFunction = createClonedTheme;
59 | break;
60 | case 'child':
61 | constructionFunction = createChildTheme;
62 | break;
63 | }
64 |
65 | if ( ! constructionFunction ) {
66 | return;
67 | }
68 | constructionFunction( theme )
69 | .then( () => {
70 | // eslint-disable-next-line no-alert
71 | window.alert(
72 | __(
73 | 'Theme created successfully. The editor will now load.',
74 | 'create-block-theme'
75 | )
76 | );
77 | window.location = window.cbt_landingpage_variables.editor_url;
78 | } )
79 | .catch( ( error ) => {
80 | setErrorMessage(
81 | error.message ||
82 | __(
83 | 'An error occurred while attempting to create the theme.',
84 | 'create-block-theme'
85 | )
86 | );
87 | } );
88 | };
89 |
90 | if ( errorMessage ) {
91 | return (
92 |
96 | { errorMessage }
97 |
98 | );
99 | }
100 |
101 | return (
102 |
106 |
107 |
108 | { __(
109 | "Let's get started creating a new Block Theme.",
110 | 'create-block-theme'
111 | ) }
112 |
113 |
123 | setTheme( { ...theme, name: value } )
124 | }
125 | help={ __(
126 | '(Tip: You can edit all of this and more in the Editor later.)',
127 | 'create-block-theme'
128 | ) }
129 | />
130 |
135 | setTheme( { ...theme, description: value } )
136 | }
137 | placeholder={ __(
138 | 'A short description of the theme',
139 | 'create-block-theme'
140 | ) }
141 | />
142 |
148 | setTheme( { ...theme, author: value } )
149 | }
150 | placeholder={ __(
151 | 'the WordPress team',
152 | 'create-block-theme'
153 | ) }
154 | />
155 |
156 | createBlockTheme() }
160 | >
161 | { renderCreateButtonText( creationType ) }
162 |
163 |
164 |
165 |
166 | );
167 | };
168 |
--------------------------------------------------------------------------------
/src/landing-page/landing-page.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { sprintf, __ } from '@wordpress/i18n';
5 | import { useState, createInterpolateElement } from '@wordpress/element';
6 | import { store as coreStore } from '@wordpress/core-data';
7 | import { useSelect } from '@wordpress/data';
8 | import {
9 | Button,
10 | ExternalLink,
11 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
12 | __experimentalVStack as VStack,
13 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
14 | __experimentalHStack as HStack,
15 | } from '@wordpress/components';
16 |
17 | /**
18 | * Internal dependencies
19 | */
20 | import { downloadExportedTheme } from '../resolvers';
21 | import downloadFile from '../utils/download-file';
22 | import { CreateThemeModal } from './create-modal';
23 |
24 | export default function LandingPage() {
25 | const [ createModalType, setCreateModalType ] = useState( false );
26 |
27 | const themeName = useSelect( ( select ) =>
28 | select( coreStore ).getCurrentTheme()
29 | )?.name?.raw;
30 |
31 | const handleExportClick = async () => {
32 | const response = await downloadExportedTheme();
33 | downloadFile( response );
34 | };
35 |
36 | return (
37 |
38 | { createModalType && (
39 |
setCreateModalType( false ) }
42 | />
43 | ) }
44 |
45 |
46 |
53 |
54 |
55 |
59 |
63 |
64 | { __(
65 | 'What would you like to do?',
66 | 'create-block-theme'
67 | ) }
68 |
69 |
70 | { createInterpolateElement(
71 | __(
72 | 'You can do everything from within the Editor but here are a few things you can do to get started.',
73 | 'create-block-theme'
74 | ),
75 | {
76 | a: (
77 | // eslint-disable-next-line jsx-a11y/anchor-has-content
78 |
84 | ),
85 | }
86 | ) }
87 |
88 | handleExportClick() }
91 | >
92 | { sprintf(
93 | // translators: %s: theme name.
94 | __(
95 | 'Export "%s" as a Zip File',
96 | 'create-block-theme'
97 | ),
98 | themeName
99 | ) }
100 |
101 |
102 | { __(
103 | 'Export a zip file ready to be imported into another WordPress environment.',
104 | 'create-block-theme'
105 | ) }
106 |
107 | setCreateModalType( 'blank' ) }
110 | >
111 | { __(
112 | 'Create a new Blank Theme',
113 | 'create-block-theme'
114 | ) }
115 |
116 |
117 | { __(
118 | 'Start from scratch! Create a blank theme to get started with your own design ideas.',
119 | 'create-block-theme'
120 | ) }
121 |
122 | setCreateModalType( 'clone' ) }
125 | >
126 | { sprintf(
127 | // translators: %s: theme name.
128 | __(
129 | 'Create a Clone of "%s"',
130 | 'create-block-theme'
131 | ),
132 | themeName
133 | ) }
134 |
135 |
136 | { __(
137 | 'Use the currently activated theme as a starting point.',
138 | 'create-block-theme'
139 | ) }
140 |
141 | setCreateModalType( 'child' ) }
144 | >
145 | { sprintf(
146 | // translators: %s: theme name.
147 | __(
148 | 'Create a Child of "%s"',
149 | 'create-block-theme'
150 | ),
151 | themeName
152 | ) }
153 |
154 |
155 | { __(
156 | 'Make a theme that uses the currently activated theme as a parent.',
157 | 'create-block-theme'
158 | ) }
159 |
160 |
161 |
162 | { __( 'About the Plugin', 'create-block-theme' ) }
163 |
164 | { __(
165 | "Create Block Theme is a tool to help you make Block Themes using the WordPress Editor. It does this by adding tools to the Editor to help you create and manage your theme. Themes created with Create Block Theme don't require Create Block Theme to be installed on the site where the theme is used.",
166 | 'create-block-theme'
167 | ) }
168 |
169 |
170 | { __( 'Do you need some help?', 'create-block-theme' ) }
171 |
172 |
173 | { createInterpolateElement(
174 | __(
175 | 'Have a question? Ask for some help in the forums .',
176 | 'create-block-theme'
177 | ),
178 | {
179 | ExternalLink: (
180 |
186 | ),
187 | }
188 | ) }
189 |
190 |
191 | { createInterpolateElement(
192 | __(
193 | 'Found a bug? Report it on GitHub .',
194 | 'create-block-theme'
195 | ),
196 | {
197 | ExternalLink: (
198 |
199 | ),
200 | }
201 | ) }
202 |
203 |
204 | { createInterpolateElement(
205 | __(
206 | 'Want to contribute? Check out the project on GitHub .',
207 | 'create-block-theme'
208 | ),
209 | {
210 | ExternalLink: (
211 |
212 | ),
213 | }
214 | ) }
215 |
216 |
217 |
{ __( 'FAQ', 'create-block-theme' ) }
218 |
219 |
220 | { __(
221 | 'How do I access the features of Create Block Theme from within the editor?',
222 | 'create-block-theme'
223 | ) }
224 |
225 |
226 | { __(
227 | 'There is a new panel accessible from the WordPress Editor which you can open by clicking on a new icon to the right of the “Save” button, at the top of the Editor.',
228 | 'create-block-theme'
229 | ) }
230 |
231 |
241 |
242 |
243 |
244 | { __(
245 | 'How do I save the customizations I made with the Editor to the Theme?',
246 | 'create-block-theme'
247 | ) }
248 |
249 |
250 | { __(
251 | 'In the Create Block Theme Panel click "Save Changes to Theme". You will be presented with a number of options of which things you want to be saved to your theme. Make your choices and then click "Save Changes".',
252 | 'create-block-theme'
253 | ) }
254 |
255 |
265 |
266 |
267 |
268 | { __(
269 | 'How do I install and remove fonts?',
270 | 'create-block-theme'
271 | ) }
272 |
273 |
274 | { __(
275 | 'First Install and activate a font from any source using the WordPress Font Library. Then, using the Create Block Theme Panel select “Save Changes To Theme” and select “Save Fonts” before saving the theme. All of the active fonts will be activated in the theme and deactivated in the system (and may be safely deleted from the system). Any fonts that are installed in the theme that have been deactivated with the WordPress Font Library will be removed from the theme.',
276 | 'create-block-theme'
277 | ) }
278 |
279 |
289 |
290 |
291 |
292 |
293 |
294 | );
295 | }
296 |
--------------------------------------------------------------------------------
/src/plugin-styles.scss:
--------------------------------------------------------------------------------
1 | @import "~@wordpress/base-styles/colors";
2 |
3 | $plugin-prefix: "create-block-theme";
4 | $modal-footer-height: 70px;
5 |
6 | .#{$plugin-prefix} {
7 | &__metadata-editor-modal {
8 | padding-bottom: $modal-footer-height;
9 |
10 | &__footer {
11 | border-top: 1px solid #ddd;
12 | background-color: #fff;
13 | position: absolute;
14 | bottom: 0;
15 | margin: 0 -32px;
16 | padding: 16px 32px;
17 | height: $modal-footer-height;
18 | }
19 |
20 | &__screenshot {
21 | max-width: 200px;
22 | height: auto;
23 | aspect-ratio: 4 / 3;
24 | object-fit: cover;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/resolvers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import apiFetch from '@wordpress/api-fetch';
5 |
6 | export async function fetchThemeJson() {
7 | return apiFetch( {
8 | path: '/create-block-theme/v1/get-theme-data',
9 | method: 'GET',
10 | headers: {
11 | 'Content-Type': 'application/json',
12 | },
13 | } ).then( ( response ) => {
14 | if ( ! response?.data || 'SUCCESS' !== response?.status ) {
15 | throw new Error(
16 | `Failed to fetch theme data: ${
17 | response?.message || response?.status
18 | }`
19 | );
20 | }
21 | return JSON.stringify( response?.data, null, 2 );
22 | } );
23 | }
24 |
25 | export async function createBlankTheme( theme ) {
26 | return apiFetch( {
27 | path: '/create-block-theme/v1/create-blank',
28 | method: 'POST',
29 | data: theme,
30 | headers: {
31 | 'Content-Type': 'application/json',
32 | },
33 | } ).then( ( response ) => {
34 | if ( 'SUCCESS' !== response?.status ) {
35 | throw new Error(
36 | `Failed to create blank theme: ${
37 | response?.message || response?.status
38 | }`
39 | );
40 | }
41 | return response;
42 | } );
43 | }
44 |
45 | export async function createClonedTheme( theme ) {
46 | return apiFetch( {
47 | path: '/create-block-theme/v1/clone',
48 | method: 'POST',
49 | data: theme,
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | },
53 | } ).then( ( response ) => {
54 | if ( 'SUCCESS' !== response?.status ) {
55 | throw new Error(
56 | `Failed to clone theme: ${
57 | response?.message || response?.status
58 | }`
59 | );
60 | }
61 | return response;
62 | } );
63 | }
64 |
65 | export async function createChildTheme( theme ) {
66 | return apiFetch( {
67 | path: '/create-block-theme/v1/create-child',
68 | method: 'POST',
69 | data: theme,
70 | headers: {
71 | 'Content-Type': 'application/json',
72 | },
73 | } ).then( ( response ) => {
74 | if ( 'SUCCESS' !== response?.status ) {
75 | throw new Error(
76 | `Failed to create child theme: ${
77 | response?.message || response?.status
78 | }`
79 | );
80 | }
81 | return response;
82 | } );
83 | }
84 |
85 | export async function fetchReadmeData() {
86 | return apiFetch( {
87 | path: '/create-block-theme/v1/get-readme-data',
88 | method: 'GET',
89 | headers: {
90 | 'Content-Type': 'application/json',
91 | },
92 | } ).then( ( response ) => {
93 | if ( ! response?.data || 'SUCCESS' !== response?.status ) {
94 | throw new Error(
95 | `Failed to fetch readme data: ${
96 | response?.message || response?.status
97 | }`
98 | );
99 | }
100 | return response?.data;
101 | } );
102 | }
103 |
104 | export async function postCreateThemeVariation( preferences ) {
105 | return apiFetch( {
106 | path: '/create-block-theme/v1/create-variation',
107 | method: 'POST',
108 | data: preferences,
109 | headers: {
110 | 'Content-Type': 'application/json',
111 | },
112 | } );
113 | }
114 |
115 | export async function postUpdateThemeMetadata( theme ) {
116 | return apiFetch( {
117 | path: '/create-block-theme/v1/update',
118 | method: 'POST',
119 | data: theme,
120 | headers: {
121 | 'Content-Type': 'application/json',
122 | },
123 | } );
124 | }
125 |
126 | export async function downloadExportedTheme() {
127 | return apiFetch( {
128 | path: '/create-block-theme/v1/export',
129 | method: 'POST',
130 | headers: {
131 | 'Content-Type': 'application/json',
132 | },
133 | parse: false,
134 | } );
135 | }
136 |
137 | export async function getFontFamilies() {
138 | const response = await apiFetch( {
139 | path: '/create-block-theme/v1/font-families',
140 | method: 'GET',
141 | headers: {
142 | 'Content-Type': 'application/json',
143 | },
144 | } );
145 | return response.data;
146 | }
147 |
148 | export async function resetTheme( preferences ) {
149 | return apiFetch( {
150 | path: '/create-block-theme/v1/reset-theme',
151 | method: 'PATCH',
152 | data: preferences,
153 | headers: {
154 | 'Content-Type': 'application/json',
155 | },
156 | } ).then( ( response ) => {
157 | if ( 'SUCCESS' !== response?.status ) {
158 | throw new Error(
159 | `Failed to reset theme: ${
160 | response?.message || response?.status
161 | }`
162 | );
163 | }
164 | return response;
165 | } );
166 | }
167 |
--------------------------------------------------------------------------------
/src/test/unit.js:
--------------------------------------------------------------------------------
1 | // TODO: Add unit tests as needed
2 |
3 | describe( 'Sample Unit Test', function () {
4 | it( 'should pass', function () {
5 | expect( true ).toBe( true );
6 | } );
7 | } );
8 |
--------------------------------------------------------------------------------
/src/utils/download-file.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | import { downloadBlob } from '@wordpress/blob';
5 |
6 | /*
7 | * Download a file from in a browser.
8 | *
9 | * @param {Response} response The response object from a fetch request.
10 | * @return {void}
11 | */
12 | export default async function downloadFile( response ) {
13 | const blob = await response.blob();
14 | const filename = response.headers
15 | .get( 'Content-Disposition' )
16 | .split( 'filename=' )[ 1 ];
17 | downloadBlob( filename, blob, 'application/zip' );
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/fonts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal dependencies
3 | */
4 | import { getFontFamilies } from '../resolvers';
5 | import { Font } from '../lib/lib-font/lib-font.browser';
6 |
7 | /**
8 | * Fetch a file from a URL and return it as an ArrayBuffer.
9 | *
10 | * @param {string} url The URL of the file to fetch.
11 | * @return {Promise} The file as an ArrayBuffer.
12 | */
13 | async function fetchFileAsArrayBuffer( url ) {
14 | const response = await fetch( url );
15 | if ( ! response.ok ) {
16 | throw new Error( 'Network response was not ok.' );
17 | }
18 | const arrayBuffer = await response.arrayBuffer();
19 | return arrayBuffer;
20 | }
21 |
22 | /**
23 | * Retrieves the licensing information of a font file given its URL.
24 | *
25 | * This function fetches the file as an ArrayBuffer, initializes a font object, and extracts licensing details from the font's OpenType tables.
26 | *
27 | * @param {string} url - The URL pointing directly to the font file. The URL should be a direct link to the file and publicly accessible.
28 | * @return {Promise} A promise that resolves to an object containing the font's licensing details.
29 | *
30 | * The returned object includes the following properties (if available in the font's OpenType tables):
31 | * - fontName: The full font name.
32 | * - copyright: Copyright notice.
33 | * - source: Unique identifier for the font's source.
34 | * - license: License description.
35 | * - licenseURL: URL to the full license text.
36 | */
37 | async function getFontFileLicenseFromUrl( url ) {
38 | const buffer = await fetchFileAsArrayBuffer( url );
39 | const fontObj = new Font( 'Uploaded Font' );
40 | fontObj.fromDataBuffer( buffer, url );
41 | // Assuming that fromDataBuffer triggers onload event and returning a Promise
42 | const onloadEvent = await new Promise(
43 | ( resolve ) => ( fontObj.onload = resolve )
44 | );
45 | const font = onloadEvent.detail.font;
46 | const { name: nameTable } = font.opentype.tables;
47 | return {
48 | fontName: nameTable.get( 16 ) || nameTable.get( 1 ),
49 | copyright: nameTable.get( 0 ),
50 | source: nameTable.get( 11 ),
51 | license: nameTable.get( 13 ),
52 | licenseURL: nameTable.get( 14 ),
53 | };
54 | }
55 |
56 | /**
57 | * Get the license for a font family.
58 | *
59 | * @param {Object} fontFamily The font family in theme.json format.
60 | * @return {Promise} A promise that resolved to the font license object if sucessful or null if the font family does not have a fontFace property.
61 | */
62 | async function getFamilyLicense( fontFamily ) {
63 | // If the font family does not have a fontFace property, return an empty string.
64 | if ( ! fontFamily.fontFace?.length ) {
65 | return null;
66 | }
67 |
68 | // Load the fontFace from the first fontFace object in the font family.
69 | const fontFace = fontFamily.fontFace[ 0 ];
70 | const faceUrl = Array.isArray( fontFace.src )
71 | ? fontFace.src[ 0 ]
72 | : fontFace.src;
73 |
74 | // Get the license from the font face url.
75 | return await getFontFileLicenseFromUrl( faceUrl );
76 | }
77 |
78 | /**
79 | * Get the text for the font licenses of all the fonts defined in the theme.
80 | *
81 | * @return {Promise} A promise that resolves to an array containing font credits objects.
82 | */
83 | async function getFontsCreditsArray() {
84 | const fontFamilies = await getFontFamilies();
85 |
86 | //Remove duplicates. Removes the font families that have the same fontFamily property.
87 | const uniqueFontFamilies = fontFamilies.filter(
88 | ( fontFamily, index, self ) =>
89 | index ===
90 | self.findIndex( ( t ) => t.fontFamily === fontFamily.fontFamily )
91 | );
92 |
93 | const credits = [];
94 |
95 | // Iterate over fontFamilies and get the license for each family
96 | for ( const fontFamily of uniqueFontFamilies ) {
97 | const fontCredits = await getFamilyLicense( fontFamily );
98 | if ( fontCredits ) {
99 | credits.push( fontCredits );
100 | }
101 | }
102 |
103 | return credits;
104 | }
105 |
106 | /**
107 | * Get the text for the font licenses of all the fonts defined in the theme.
108 | *
109 | * @return {Promise} A promise that resolves to an string containing the formatted font licenses.
110 | */
111 | export async function getFontsCreditsText() {
112 | const creditsArray = await getFontsCreditsArray();
113 | const credits = creditsArray
114 | .reduce( ( acc, credit ) => {
115 | // skip if fontName is not available
116 | if ( ! credit.fontName ) {
117 | // continue
118 | return acc;
119 | }
120 |
121 | acc.push( credit.fontName );
122 |
123 | if ( credit.copyright ) {
124 | acc.push( credit.copyright );
125 | }
126 |
127 | if ( credit.source ) {
128 | acc.push( `Source: ${ credit.source }` );
129 | }
130 |
131 | if ( credit.license ) {
132 | acc.push( `License: ${ credit.license }` );
133 | }
134 |
135 | acc.push( '' );
136 |
137 | return acc;
138 | }, [] )
139 | .join( '\n' );
140 | return credits;
141 | }
142 |
--------------------------------------------------------------------------------
/src/utils/generate-versions.js:
--------------------------------------------------------------------------------
1 | export function generateWpVersions( versionString ) {
2 | const version = versionString.split( '-' )[ 0 ];
3 | let [ major, minor ] = version.split( '.' ).slice( 0, 2 ).map( Number );
4 |
5 | const versions = [];
6 |
7 | // Iterate through the versions from current to 5.9
8 | while ( major > 5 || ( major === 5 && minor >= 9 ) ) {
9 | versions.push( `${ major }.${ minor }` );
10 |
11 | // Decrement minor version
12 | if ( minor === 0 ) {
13 | minor = 9; // Wrap around if minor is 0, decrement the major version
14 | major--;
15 | } else {
16 | minor--;
17 | }
18 | }
19 |
20 | return versions;
21 | }
22 |
--------------------------------------------------------------------------------
/test/unit/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jsdom',
3 | rootDir: '../../',
4 | testMatch: [ '/src/test/**/*.js' ],
5 | moduleFileExtensions: [ 'js' ],
6 | moduleNameMapper: {
7 | '^@/(.*)$': '/src/$1',
8 | },
9 | setupFiles: [ '/test/unit/setup.js' ],
10 | };
11 |
--------------------------------------------------------------------------------
/test/unit/setup.js:
--------------------------------------------------------------------------------
1 | // Stub out the FontFace class for tests.
2 | global.FontFace = class {
3 | constructor( family, source ) {
4 | this.family = family;
5 | this.source = source;
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/tests/CbtThemeLocale/base.php:
--------------------------------------------------------------------------------
1 | orig_active_theme_slug = get_option( 'stylesheet' );
31 |
32 | // Create a test theme directory.
33 | $this->test_theme_dir = DIR_TESTDATA . '/themes/';
34 |
35 | // Register test theme directory.
36 | register_theme_directory( $this->test_theme_dir );
37 |
38 | // Switch to the test theme.
39 | switch_theme( 'test-theme-locale' );
40 | }
41 |
42 | /**
43 | * Tears down tests.
44 | */
45 | public function tear_down() {
46 | parent::tear_down();
47 |
48 | // Restore the original active theme.
49 | switch_theme( $this->orig_active_theme_slug );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/CbtThemeLocale/escapeAttribute.php:
--------------------------------------------------------------------------------
1 | getMethod( $method_name );
16 | $method->setAccessible( true );
17 | return $method->invokeArgs( null, $args );
18 | }
19 |
20 | public function test_escape_attribute() {
21 | $string = 'This is a test attribute.';
22 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) );
23 | $expected_string = "get( 'TextDomain' ) . "');?>";
24 | $this->assertEquals( $expected_string, $escaped_string );
25 | }
26 |
27 | public function test_escape_attribute_with_single_quote() {
28 | $string = "This is a test attribute with a single quote '";
29 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) );
30 | $expected_string = "get( 'TextDomain' ) . "');?>";
31 | $this->assertEquals( $expected_string, $escaped_string );
32 | }
33 |
34 | public function test_escape_attribute_with_double_quote() {
35 | $string = 'This is a test attribute with a double quote "';
36 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) );
37 | $expected_string = "get( 'TextDomain' ) . "');?>";
38 | $this->assertEquals( $expected_string, $escaped_string );
39 | }
40 |
41 | public function test_escape_attribute_with_empty_string() {
42 | $string = '';
43 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) );
44 | $this->assertEquals( $string, $escaped_string );
45 | }
46 |
47 | public function test_escape_attribute_with_already_escaped_string() {
48 | $string = "get( 'TextDomain' ) . "');?>";
49 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) );
50 | $this->assertEquals( $string, $escaped_string );
51 | }
52 |
53 | public function test_escape_attribute_with_non_string() {
54 | $string = null;
55 | $escaped_string = $this->call_private_method( 'escape_attribute', array( $string ) );
56 | $this->assertEquals( $string, $escaped_string );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/CbtThemeLocale/escapeTextContent.php:
--------------------------------------------------------------------------------
1 | getMethod( $method_name );
17 | $method->setAccessible( true );
18 | return $method->invokeArgs( null, $args );
19 | }
20 |
21 | public function test_escape_text_content() {
22 | $string = 'This is a test text.';
23 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) );
24 | $this->assertEquals( "", $escaped_string );
25 | }
26 |
27 | public function test_escape_text_content_with_single_quote() {
28 | $string = "This is a test text with a single quote '";
29 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) );
30 | $this->assertEquals( "", $escaped_string );
31 | }
32 |
33 | public function test_escape_text_content_with_double_quote() {
34 | $string = 'This is a test text with a double quote "';
35 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) );
36 | $this->assertEquals( "", $escaped_string );
37 | }
38 |
39 | public function test_escape_text_content_with_html() {
40 | $string = 'This is a test text with HTML.
';
41 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) );
42 | $expected_output = '\', \'\' ); ?>';
43 | $this->assertEquals( $expected_output, $escaped_string );
44 | }
45 |
46 | public function test_escape_text_content_with_already_escaped_string() {
47 | $string = "";
48 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) );
49 | $this->assertEquals( $string, $escaped_string );
50 | }
51 |
52 | public function test_escape_text_content_with_non_string() {
53 | $string = null;
54 | $escaped_string = $this->call_private_method( 'escape_text_content', array( $string ) );
55 | $this->assertEquals( $string, $escaped_string );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/CbtThemeReadme/addOrUpdateSection.php:
--------------------------------------------------------------------------------
1 | assertStringContainsString( $section_title, $readme, 'The section title is missing.' );
23 | $this->assertStringContainsString( $section_content, $readme, 'The section content is missing' );
24 |
25 | // Update the section.
26 | $section_content_updated = 'Updated content xyz890';
27 |
28 | $readme = CBT_Theme_Readme::add_or_update_section( $section_title, $section_content_updated );
29 |
30 | // Check if the old content was updated.
31 | $this->assertStringNotContainsString( $section_content, $readme, 'The old content is still present.' );
32 |
33 | // Check if the new content was added.
34 | $this->assertStringContainsString( $section_title, $readme, 'The section title is missing.' );
35 | $this->assertStringContainsString( $section_content_updated, $readme, 'The updated content is missing.' );
36 |
37 | // Check if that the section title was added only once.
38 | $section_count = substr_count( $readme, $section_title );
39 | $this->assertEquals( 1, $section_count, 'The section title was added more than once.' );
40 | }
41 |
42 | public function test_add_or_update_section_with_no_content() {
43 | $section_title = 'Test Section';
44 | $section_content = '';
45 |
46 | // Empty section should not be added.
47 | $readme = CBT_Theme_Readme::add_or_update_section( $section_title, $section_content );
48 | $this->assertStringNotContainsString( $section_title, $readme, 'The title of an empty section should not be added.' );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/CbtThemeReadme/base.php:
--------------------------------------------------------------------------------
1 | orig_active_theme_slug = get_option( 'stylesheet' );
38 |
39 | // Create a test theme directory.
40 | $this->test_theme_dir = DIR_TESTDATA . '/themes/';
41 |
42 | // Register test theme directory.
43 | register_theme_directory( $this->test_theme_dir );
44 |
45 | // Switch to the test theme.
46 | switch_theme( 'test-theme-readme' );
47 |
48 | // Store the original readme.txt content.
49 | $this->orig_readme_content = CBT_Theme_Readme::get_content();
50 | }
51 |
52 | /**
53 | * Tears down tests.
54 | */
55 | public function tear_down() {
56 | parent::tear_down();
57 |
58 | // Restore the original readme.txt content.
59 | file_put_contents( CBT_Theme_Readme::file_path(), $this->orig_readme_content );
60 |
61 | // Restore the original active theme.
62 | switch_theme( $this->orig_active_theme_slug );
63 | }
64 |
65 | /**
66 | * Removes the newlines from a string.
67 | *
68 | * This is useful to make it easier to search for strings in the readme content.
69 | * Removes both DOS and Unix newlines.
70 | *
71 | * @param string $string
72 | * @return string
73 | */
74 | public function remove_newlines( $string ) {
75 | return str_replace( array( "\r\n", "\n" ), '', $string );
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/tests/CbtThemeReadme/filePath.php:
--------------------------------------------------------------------------------
1 | assertEquals( $expected, $result );
17 |
18 | $this->assertEquals( 'test-theme-readme', get_option( 'stylesheet' ) );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/CbtThemeReadme/getContent.php:
--------------------------------------------------------------------------------
1 | assertEquals( $expected, $result );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/CbtThemeReadme/update.php:
--------------------------------------------------------------------------------
1 | assertStringNotContainsString( "\r\n", $readme, 'The readme content contains DOS newlines.' );
23 |
24 | // Removes the newlines from the readme content to make it easier to search for strings.
25 | $readme_without_newlines = $this->remove_newlines( $readme );
26 |
27 | $expected_author = 'Contributors: ' . $data['author'];
28 | $expected_wp_version = 'Tested up to: ' . $data['wp_version'] ?? CBT_Theme_Utils::get_current_wordpress_version();
29 | $expected_image_credits = '== Images ==' . $this->remove_newlines( $data['image_credits'] );
30 | $expected_recommended_plugins = '== Recommended Plugins ==' . $this->remove_newlines( $data['recommended_plugins'] );
31 |
32 | $this->assertStringContainsString( $expected_author, $readme_without_newlines, 'The expected author is missing.' );
33 | $this->assertStringContainsString( $expected_wp_version, $readme_without_newlines, 'The expected WP version is missing.' );
34 | $this->assertStringContainsString( $expected_image_credits, $readme_without_newlines, 'The expected image credits are missing.' );
35 | $this->assertStringContainsString( $expected_recommended_plugins, $readme_without_newlines, 'The expected recommended plugins are missing.' );
36 |
37 | // Assertion specific to font credits.
38 | if ( isset( $data['font_credits'] ) ) {
39 | $expected_font_credits = '== Fonts ==' . $this->remove_newlines( $data['font_credits'] );
40 | $this->assertStringContainsString( $expected_font_credits, $readme_without_newlines, 'The expected font credits are missing.' );
41 | }
42 | }
43 |
44 | public function data_test_update() {
45 | return array(
46 | 'complete data' => array(
47 | 'data' => array(
48 | 'description' => 'New theme description',
49 | 'author' => 'New theme author',
50 | 'wp_version' => '12.12',
51 | 'requires_wp' => '',
52 | 'image_credits' => 'New image credits',
53 | 'recommended_plugins' => 'New recommended plugins',
54 | 'font_credits' => 'Example font credits text',
55 | ),
56 | ),
57 | 'missing font credits' => array(
58 | 'data' => array(
59 | 'description' => 'New theme description',
60 | 'author' => 'New theme author',
61 | 'wp_version' => '12.12',
62 | 'requires_wp' => '',
63 | 'image_credits' => 'New image credits',
64 | 'recommended_plugins' => 'New recommended plugins',
65 | ),
66 | ),
67 | /*
68 | * This string contains DOS newlines.
69 | * It uses double quotes to make PHP interpret the newlines as newlines and not as string literals.
70 | */
71 | 'Remove DOS newlines' => array(
72 | 'data' => array(
73 | 'description' => 'New theme description',
74 | 'author' => 'New theme author',
75 | 'wp_version' => '12.12',
76 | 'requires_wp' => '',
77 | 'image_credits' => "New image credits \r\n New image credits 2",
78 | 'recommended_plugins' => "Plugin1 \r\n Plugin2 \r\n Plugin3",
79 | 'font_credits' => "Font1 \r\n Font2 \r\n Font3",
80 | ),
81 | ),
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | content = '
10 |
11 |
12 |
13 | ';
14 | $new_template = CBT_Theme_Media::make_template_images_local( $template );
15 |
16 | // The image should be replaced with a relative URL
17 | $this->assertStringNotContainsString( 'http://example.com/image.jpg', $new_template->content );
18 | $this->assertStringContainsString( 'get_template_directory_uri', $new_template->content );
19 | $this->assertStringContainsString( '/assets/images', $new_template->content );
20 |
21 | }
22 |
23 | public function test_make_cover_block_local() {
24 | $template = new stdClass();
25 | $template->content = '
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ';
34 | $new_template = CBT_Theme_Media::make_template_images_local( $template );
35 |
36 | // The image should be replaced with a relative URL
37 | $this->assertStringNotContainsString( 'http://example.com/image.jpg', $new_template->content );
38 | $this->assertStringContainsString( 'get_template_directory_uri', $new_template->content );
39 | $this->assertStringContainsString( '/assets/images', $new_template->content );
40 | }
41 |
42 | public function test_template_with_media_correctly_prepared() {
43 | $template = new stdClass();
44 | $template->slug = 'test-template';
45 | $template->content = '
46 |
47 |
48 |
49 | ';
50 | $new_template = CBT_Theme_Templates::prepare_template_for_export( $template );
51 |
52 | // Content should be replaced with a pattern block
53 | $this->assertStringContainsString( '
68 |
69 |
70 | ';
71 | $new_template = CBT_Theme_Templates::prepare_template_for_export( $template );
72 |
73 | // Content should be replaced with a pattern block
74 | $this->assertStringContainsString( '
16 | ';
17 |
18 | $updated_pattern_string = CBT_Theme_Utils::replace_namespace( $pattern_string, 'old-slug', 'new-slug', 'Old Name', 'New Name' );
19 | $this->assertStringContainsString( 'Slug: new-slug/index', $updated_pattern_string );
20 | $this->assertStringNotContainsString( 'old-slug', $updated_pattern_string );
21 |
22 | }
23 |
24 | public function test_replace_namespace_in_code() {
25 | $code_string = "assertStringContainsString( '@package new-slug', $updated_code_string );
40 | $this->assertStringNotContainsString( 'old-slug', $updated_code_string );
41 | $this->assertStringContainsString( 'function new_slug_support', $updated_code_string );
42 | $this->assertStringContainsString( "function_exists( 'new_slug_support' )", $updated_code_string );
43 | }
44 |
45 | public function test_replace_namespace_in_code_with_single_word_slug() {
46 | $code_string = "assertStringContainsString( '@package new-slug', $updated_code_string );
61 | $this->assertStringNotContainsString( 'old-slug', $updated_code_string );
62 | $this->assertStringContainsString( 'function new_slug_support', $updated_code_string );
63 | $this->assertStringContainsString( "function_exists( 'new_slug_support' )", $updated_code_string );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/update-version-and-changelog.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | /**
4 | * External dependencies
5 | */
6 | const fs = require( 'fs' );
7 | const core = require( '@actions/core' );
8 | const simpleGit = require( 'simple-git' );
9 | const { promisify } = require( 'util' );
10 | const exec = promisify( require( 'child_process' ).exec );
11 |
12 | const git = simpleGit.default();
13 |
14 | const releaseType = process.env.RELEASE_TYPE;
15 | const VALID_RELEASE_TYPES = [ 'major', 'minor', 'patch' ];
16 |
17 | // To get the merges since the last (previous) tag
18 | async function getChangesSinceLastTag() {
19 | try {
20 | // Fetch all tags, sorted by creation date
21 | const tagsResult = await git.tags( {
22 | '--sort': '-creatordate',
23 | } );
24 | const tags = tagsResult.all;
25 | if ( tags.length === 0 ) {
26 | console.error( '❌ Error: No previous tags found.' );
27 | return null;
28 | }
29 | const previousTag = tags[ 0 ]; // The most recent tag
30 |
31 | // Now get the changes since this tag
32 | const changes = await git.log( [ `${ previousTag }..HEAD` ] );
33 | return changes;
34 | } catch ( error ) {
35 | throw error;
36 | }
37 | }
38 |
39 | // To know if there are changes since the last tag.
40 | // we are not using getChangesSinceGitTag because it returns the just the merges and not the commits.
41 | // So for example if a hotfix was committed directly to trunk this function will detect it but getChangesSinceGitTag will not.
42 | async function getHasChangesSinceGitTag( tag ) {
43 | const changes = await git.log( [ `HEAD...${ tag }` ] );
44 | return changes?.all?.length > 0;
45 | }
46 |
47 | async function updateVersion() {
48 | if ( ! VALID_RELEASE_TYPES.includes( releaseType ) ) {
49 | console.error(
50 | '❌ Error: Release type is not valid. Valid release types are: major, minor, patch.'
51 | );
52 | process.exit( 1 );
53 | }
54 |
55 | if (
56 | ! fs.existsSync( './package.json' ) ||
57 | ! fs.existsSync( './package-lock.json' )
58 | ) {
59 | console.error( '❌ Error: package.json or lock file not found.' );
60 | process.exit( 1 );
61 | }
62 |
63 | if ( ! fs.existsSync( './readme.txt' ) ) {
64 | console.error( '❌ Error: readme.txt file not found.' );
65 | process.exit( 1 );
66 | }
67 |
68 | if ( ! fs.existsSync( './create-block-theme.php' ) ) {
69 | console.error( '❌ Error: create-block-theme.php file not found.' );
70 | process.exit( 1 );
71 | }
72 |
73 | // get changes since last tag
74 | let changes = [];
75 | try {
76 | changes = await getChangesSinceLastTag();
77 | } catch ( error ) {
78 | console.error(
79 | `❌ Error: failed to get changes since last tag: ${ error }`
80 | );
81 | process.exit( 1 );
82 | }
83 |
84 | const packageJson = require( './package.json' );
85 | const currentVersion = packageJson.version;
86 |
87 | // version bump package.json and package-lock.json using npm
88 | const { stdout, stderr } = await exec(
89 | `npm version --commit-hooks false --git-tag-version false ${ releaseType }`
90 | );
91 | if ( stderr ) {
92 | console.error( `❌ Error: failed to bump the version."` );
93 | process.exit( 1 );
94 | }
95 |
96 | const currentTag = `v${ currentVersion }`;
97 | const newTag = stdout.trim();
98 | const newVersion = newTag.replace( 'v', '' );
99 | const hasChangesSinceGitTag = await getHasChangesSinceGitTag( currentTag );
100 |
101 | // check if there are any changes
102 | if ( ! hasChangesSinceGitTag ) {
103 | console.error(
104 | `❌ No changes since last tag (${ currentTag }). There is nothing new to release.`
105 | );
106 | // revert version update
107 | await exec(
108 | `npm version --commit-hooks false --git-tag-version false ${ currentVersion }`
109 | );
110 | process.exit( 1 );
111 | }
112 |
113 | console.info( '✅ Package.json version updated', currentTag, '=>', newTag );
114 |
115 | // update readme.txt version with the new changelog
116 | const readme = fs.readFileSync( './readme.txt', 'utf8' );
117 | const capitalizeFirstLetter = ( string ) =>
118 | string.charAt( 0 ).toUpperCase() + string.slice( 1 );
119 |
120 | const changelogChanges = changes.all
121 | .map(
122 | ( change ) =>
123 | `* ${ capitalizeFirstLetter( change.message || change.body ) }`
124 | )
125 | .join( '\n' );
126 | const newChangelog = `== Changelog ==\n\n= ${ newVersion } =\n${ changelogChanges }`;
127 | let newReadme = readme.replace( '== Changelog ==', newChangelog );
128 | // update version in readme.txt
129 | newReadme = newReadme.replace(
130 | /Stable tag: (.*)/,
131 | `Stable tag: ${ newVersion }`
132 | );
133 | fs.writeFileSync( './readme.txt', newReadme );
134 | console.info( '✅ Readme version updated', currentTag, '=>', newTag );
135 |
136 | // update create-block-theme.php version
137 | const pluginPhpFile = fs.readFileSync( './create-block-theme.php', 'utf8' );
138 | const newPluginPhpFile = pluginPhpFile.replace(
139 | /Version: (.*)/,
140 | `Version: ${ newVersion }`
141 | );
142 | fs.writeFileSync( './create-block-theme.php', newPluginPhpFile );
143 | console.info(
144 | '✅ create-block-theme.php file version updated',
145 | currentTag,
146 | '=>',
147 | newTag
148 | );
149 |
150 | // output data to be used by the next steps of the github action
151 | core.setOutput( 'NEW_VERSION', newVersion );
152 | core.setOutput( 'NEW_TAG', newTag );
153 | core.setOutput( 'CHANGELOG', changelogChanges );
154 | }
155 |
156 | updateVersion();
157 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WordPress dependencies
3 | */
4 | const defaultConfig = require( '@wordpress/scripts/config/webpack.config.js' );
5 |
6 | module.exports = {
7 | // Default wordpress config
8 | ...defaultConfig,
9 |
10 | // custom config to avoid errors with lib-font dependency
11 | ...{
12 | resolve: {
13 | fallback: {
14 | zlib: false,
15 | fs: false,
16 | },
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------