PluginProbe
Gutenberg / 8.3.0
Gutenberg v8.3.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / global-styles.php

global-styles.php in Gutenberg 8.3.0, at lib/global-styles.php

639 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Bootstraps Global Styles.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Whether the current theme has a theme.json file.
10 *
11 * @return boolean
12 */
13 function gutenberg_experimental_global_styles_has_theme_json_support() {
14 return is_readable( locate_template( 'experimental-theme.json' ) );
15 }
16
17 /**
18 * Given a tree, it creates a flattened one
19 * by merging the keys and binding the leaf values
20 * to the new keys.
21 *
22 * This is thought to be useful to generate
23 * CSS Custom Properties from a tree,
24 * although there's nothing in the implementation
25 * of this function that requires that format.
26 *
27 * For example, assuming the given prefix is '--wp'
28 * and the token is '--', for this input tree:
29 *
30 * {
31 * 'property': 'value',
32 * 'nested-property': {
33 * 'sub-property': 'value'
34 * }
35 * }
36 *
37 * it'll return this output:
38 *
39 * {
40 * '--wp--property': 'value',
41 * '--wp--nested-property--sub-property': 'value'
42 * }
43 *
44 * @param array $tree Input tree to process.
45 * @param string $prefix Prefix to prepend to each variable. '' by default.
46 * @param string $token Token to use between levels. '--' by default.
47 *
48 * @return array The flattened tree.
49 */
50 function gutenberg_experimental_global_styles_get_css_vars( $tree, $prefix = '', $token = '--' ) {
51 $result = array();
52 foreach ( $tree as $property => $value ) {
53 $new_key = $prefix . str_replace( '/', '-', $property );
54
55 if ( is_array( $value ) ) {
56 $new_prefix = $new_key . $token;
57 $result = array_merge(
58 $result,
59 gutenberg_experimental_global_styles_get_css_vars( $value, $new_prefix, $token )
60 );
61 } else {
62 $result[ $new_key ] = $value;
63 }
64 }
65 return $result;
66 }
67
68 /**
69 * Processes a file that adheres to the theme.json
70 * schema and returns an array with its contents,
71 * or a void array if none found.
72 *
73 * @param string $file_path Path to file.
74 * @return array Contents that adhere to the theme.json schema.
75 */
76 function gutenberg_experimental_global_styles_get_from_file( $file_path ) {
77 $config = array();
78 if ( file_exists( $file_path ) ) {
79 $decoded_file = json_decode(
80 file_get_contents( $file_path ),
81 true
82 );
83
84 $json_decoding_error = json_last_error();
85 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
86 error_log( 'Error when decoding file schema: ' . json_last_error_msg() );
87 return $config;
88 }
89
90 if ( is_array( $decoded_file ) ) {
91 $config = $decoded_file;
92 }
93 }
94 return $config;
95 }
96
97 /**
98 * Returns the user's origin config.
99 *
100 * @return array Config that adheres to the theme.json schema.
101 */
102 function gutenberg_experimental_global_styles_get_user() {
103 $config = array();
104 $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( array( 'publish' ) );
105 if ( array_key_exists( 'post_content', $user_cpt ) ) {
106 $decoded_data = json_decode( $user_cpt['post_content'], true );
107
108 $json_decoding_error = json_last_error();
109 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
110 error_log( 'Error when decoding user schema: ' . json_last_error_msg() );
111 return $config;
112 }
113
114 if ( is_array( $decoded_data ) ) {
115 $config = $decoded_data;
116 }
117 }
118
119 return $config;
120 }
121
122 /**
123 * Returns the CPT that contains the user's origin config
124 * for the current theme or a void array if none found.
125 *
126 * It can also create and return a new draft CPT.
127 *
128 * @param array $post_status_filter Filter CPT by post status.
129 * ['publish'] by default, so it only fetches published posts.
130 * @param bool $should_create_draft Whether a new draft should be created if no CPT was found.
131 * False by default.
132 * @return array Custom Post Type for the user's origin config.
133 */
134 function gutenberg_experimental_global_styles_get_user_cpt( $post_status_filter = array( 'publish' ), $should_create_draft = false ) {
135 $user_cpt = array();
136 $post_type_filter = 'wp_global_styles';
137 $post_name_filter = 'wp-global-styles-' . strtolower( wp_get_theme()->get( 'TextDomain' ) );
138 $recent_posts = wp_get_recent_posts(
139 array(
140 'numberposts' => 1,
141 'orderby' => 'date',
142 'order' => 'desc',
143 'post_type' => $post_type_filter,
144 'post_status' => $post_status_filter,
145 'name' => $post_name_filter,
146 )
147 );
148
149 if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) {
150 $user_cpt = $recent_posts[0];
151 } elseif ( $should_create_draft ) {
152 $cpt_post_id = wp_insert_post(
153 array(
154 'post_content' => '{}',
155 'post_status' => 'draft',
156 'post_type' => $post_type_filter,
157 'post_name' => $post_name_filter,
158 ),
159 true
160 );
161 $user_cpt = get_post( $cpt_post_id, ARRAY_A );
162 }
163
164 return $user_cpt;
165 }
166
167 /**
168 * Returns the post ID of the CPT containing the user's origin config.
169 *
170 * @return integer
171 */
172 function gutenberg_experimental_global_styles_get_user_cpt_id() {
173 $user_cpt_id = null;
174 $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( array( 'publish', 'draft' ), true );
175 if ( array_key_exists( 'ID', $user_cpt ) ) {
176 $user_cpt_id = $user_cpt['ID'];
177 }
178 return $user_cpt_id;
179 }
180
181 /**
182 * Return core's origin config.
183 *
184 * @return array Config that adheres to the theme.json schema.
185 */
186 function gutenberg_experimental_global_styles_get_core() {
187 $config = gutenberg_experimental_global_styles_get_from_file(
188 __DIR__ . '/experimental-default-theme.json'
189 );
190
191 return $config;
192 }
193
194 /**
195 * Returns the theme presets registered via add_theme_support, if any.
196 *
197 * @return array Config that adheres to the theme.json schema.
198 */
199 function gutenberg_experimental_global_styles_get_theme_presets() {
200 $theme_presets = array();
201
202 $theme_colors = get_theme_support( 'editor-color-palette' )[0];
203 if ( is_array( $theme_colors ) ) {
204 foreach ( $theme_colors as $color ) {
205 $theme_presets['global']['presets']['color'][] = array(
206 'slug' => $color['slug'],
207 'value' => $color['color'],
208 );
209 }
210 }
211
212 $theme_gradients = get_theme_support( 'editor-gradient-presets' )[0];
213 if ( is_array( $theme_gradients ) ) {
214 foreach ( $theme_gradients as $gradient ) {
215 $theme_presets['global']['presets']['gradient'][] = array(
216 'slug' => $gradient['slug'],
217 'value' => $gradient['gradient'],
218 );
219 }
220 }
221
222 $theme_font_sizes = get_theme_support( 'editor-font-sizes' )[0];
223 if ( is_array( $theme_font_sizes ) ) {
224 foreach ( $theme_font_sizes as $font_size ) {
225 $theme_presets['global']['presets']['font-size'][] = array(
226 'slug' => $font_size['slug'],
227 'value' => $font_size['size'],
228 );
229 }
230 }
231
232 return $theme_presets;
233 }
234
235 /**
236 * Returns the theme's origin config.
237 *
238 * It also fetches the existing presets the theme declared via add_theme_support
239 * and uses them if the theme hasn't declared any via theme.json.
240 *
241 * @return array Config that adheres to the theme.json schema.
242 */
243 function gutenberg_experimental_global_styles_get_theme() {
244 $theme_presets = gutenberg_experimental_global_styles_get_theme_presets();
245 $theme_config = gutenberg_experimental_global_styles_get_from_file(
246 locate_template( 'experimental-theme.json' )
247 );
248
249 /*
250 * We want the presets declared in theme.json
251 * to take precedence over the ones declared via add_theme_support.
252 *
253 * Note that merging happens at the preset category level. Example:
254 *
255 * - if the theme declares a color palette via add_theme_support &
256 * a set of font sizes via theme.json, both will be included in the output.
257 *
258 * - if the theme declares a color palette both via add_theme_support &
259 * via theme.json, the later takes precedence.
260 *
261 */
262 $theme_config = gutenberg_experimental_global_styles_merge_trees(
263 $theme_presets,
264 $theme_config
265 );
266
267 return $theme_config;
268 }
269
270 /**
271 * Returns the style features a particular block supports.
272 *
273 * @param array $supports The block supports array.
274 *
275 * @return array Style features supported by the block.
276 */
277 function gutenberg_experimental_global_styles_get_supported_styles( $supports ) {
278 $style_features = array(
279 'color' => array( '__experimentalColor' ),
280 'background-color' => array( '__experimentalColor' ),
281 'background' => array( '__experimentalColor', 'gradients' ),
282 'line-height' => array( '__experimentalLineHeight' ),
283 'font-size' => array( '__experimentalFontSize' ),
284 );
285
286 $supported_features = array();
287 foreach ( $style_features as $style_feature => $path ) {
288 if ( gutenberg_experimental_get( $supports, $path ) ) {
289 $supported_features[] = $style_feature;
290 }
291 }
292
293 return $supported_features;
294 }
295
296 /**
297 * Retrieves the block data (selector/supports).
298 *
299 * @return array
300 */
301 function gutenberg_experimental_global_styles_get_block_data() {
302 $block_data = array(
303 'global' => array(
304 'selector' => ':root',
305 'supports' => array( 'background-color' ),
306 ),
307 );
308
309 $registry = WP_Block_Type_Registry::get_instance();
310 foreach ( $registry->get_all_registered() as $block_name => $block_type ) {
311 if ( empty( $block_type->supports ) || ! is_array( $block_type->supports ) ) {
312 continue;
313 }
314
315 $supports = gutenberg_experimental_global_styles_get_supported_styles( $block_type->supports );
316 if ( empty( $supports ) ) {
317 continue;
318 }
319
320 /*
321 * Assign the selector for the block.
322 *
323 * Some blocks can declare multiple selectors:
324 *
325 * - core/heading represents the H1-H6 HTML elements
326 * - core/list represents the UL and OL HTML elements
327 * - core/group is meant to represent DIV and other HTML elements
328 *
329 * Some other blocks don't provide a selector,
330 * so we generate a class for them based on their name:
331 *
332 * - 'core/group' => '.wp-block-group'
333 * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name'
334 *
335 * Note that, for core blocks, we don't add the `core/` prefix to its class name.
336 * This is for historical reasons, as they come with a class without that infix.
337 *
338 */
339 if (
340 isset( $block_type->supports['__experimentalSelector'] ) &&
341 is_string( $block_type->supports['__experimentalSelector'] )
342 ) {
343 $block_data[ $block_name ] = array(
344 'selector' => $block_type->supports['__experimentalSelector'],
345 'supports' => $supports,
346 );
347 } elseif (
348 isset( $block_type->supports['__experimentalSelector'] ) &&
349 is_array( $block_type->supports['__experimentalSelector'] )
350 ) {
351 foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector ) {
352 $block_data[ $key ] = array(
353 'selector' => $selector,
354 'supports' => $supports,
355 );
356 }
357 } else {
358 $block_data[ $block_name ] = array(
359 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ),
360 'supports' => $supports,
361 );
362 }
363 }
364
365 return $block_data;
366 }
367
368 /**
369 * Given an array contain the styles shape returns the css for this styles.
370 * A similar function exists on the client at /packages/block-editor/src/hooks/style.js.
371 *
372 * @param array $styles Array containing the styles shape from global styles.
373 *
374 * @return array Containing a set of css rules.
375 */
376 function gutenberg_experimental_global_styles_flatten_styles_tree( $styles ) {
377 $mappings = array(
378 'line-height' => array( 'typography', 'lineHeight' ),
379 'font-size' => array( 'typography', 'fontSize' ),
380 'background' => array( 'color', 'gradient' ),
381 'background-color' => array( 'color', 'background' ),
382 'color' => array( 'color', 'text' ),
383 '--wp--style--color--link' => array( 'color', 'link' ),
384 );
385
386 $result = array();
387
388 foreach ( $mappings as $key => $path ) {
389 $value = gutenberg_experimental_get( $styles, $path );
390 if ( null !== $value ) {
391 $result[ $key ] = $value;
392 }
393 }
394 return $result;
395
396 }
397
398 /**
399 * Takes a tree adhering to the theme.json schema and generates
400 * the corresponding stylesheet.
401 *
402 * @param array $tree Input tree.
403 *
404 * @return string Stylesheet.
405 */
406 function gutenberg_experimental_global_styles_resolver( $tree ) {
407 $stylesheet = '';
408 $block_data = gutenberg_experimental_global_styles_get_block_data();
409 foreach ( array_keys( $tree ) as $block_name ) {
410 if (
411 ! array_key_exists( $block_name, $block_data ) ||
412 ! array_key_exists( 'selector', $block_data[ $block_name ] ) ||
413 ! array_key_exists( 'supports', $block_data[ $block_name ] )
414 ) {
415 // Skip blocks that haven't declared support,
416 // because we don't know to process them.
417 continue;
418 }
419
420 // Extract the relevant preset info before converting them to CSS Custom Properties.
421 foreach ( array_keys( $tree[ $block_name ]['presets'] ) as $preset_category ) {
422 $flattened_values = array();
423 foreach ( $tree[ $block_name ]['presets'][ $preset_category ] as $preset_value ) {
424 $flattened_values[ $preset_value['slug'] ] = $preset_value['value'];
425 }
426 $tree[ $block_name ]['presets'][ $preset_category ] = $flattened_values;
427 }
428
429 $token = '--';
430 $prefix = '--wp--preset' . $token;
431 $css_variables = gutenberg_experimental_global_styles_get_css_vars( $tree[ $block_name ]['presets'], $prefix, $token );
432
433 $stylesheet .= gutenberg_experimental_global_styles_resolver_styles(
434 $block_data[ $block_name ]['selector'],
435 $block_data[ $block_name ]['supports'],
436 array_merge(
437 gutenberg_experimental_global_styles_flatten_styles_tree( $tree[ $block_name ]['styles'] ),
438 $css_variables
439 )
440 );
441 }
442
443 if ( gutenberg_experimental_global_styles_has_theme_json_support() ) {
444 // To support all themes, we added in the block-library stylesheet
445 // a style rule such as .has-link-color a { color: var(--wp--style--color--link, #00e); }
446 // so that existing link colors themes used didn't break.
447 // We add this here to make it work for themes that opt-in to theme.json
448 // In the future, we may do this differently.
449 $stylesheet .= 'a { color: var(--wp--style--color--link, #00e); }';
450 }
451
452 return $stylesheet;
453 }
454
455 /**
456 * Generates CSS declarations for a block.
457 *
458 * @param string $block_selector CSS selector for the block.
459 * @param array $block_supports A list of properties supported by the block.
460 * @param array $block_styles The list of properties/values to be converted to CSS.
461 *
462 * @return string The corresponding CSS rule.
463 */
464 function gutenberg_experimental_global_styles_resolver_styles( $block_selector, $block_supports, $block_styles ) {
465 $css_rule = '';
466 $css_declarations = '';
467
468 foreach ( $block_styles as $property => $value ) {
469 // Only convert to CSS:
470 //
471 // 1) The style attributes the block has declared support for.
472 // 2) Any CSS custom property attached to the node.
473 if ( in_array( $property, $block_supports, true ) || strstr( $property, '--' ) ) {
474 $css_declarations .= "\t" . $property . ': ' . $value . ";\n";
475 }
476 }
477 if ( '' !== $css_declarations ) {
478 $css_rule .= $block_selector . " {\n";
479 $css_rule .= $css_declarations;
480 $css_rule .= "}\n";
481 }
482
483 return $css_rule;
484 }
485
486 /**
487 * Helper function that merges trees that adhere to the theme.json schema.
488 *
489 * @param array $core Core origin.
490 * @param array $theme Theme origin.
491 * @param array $user User origin. An empty array by default.
492 *
493 * @return array The merged result.
494 */
495 function gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user = array() ) {
496 $core = gutenberg_experimental_global_styles_normalize_schema( $core );
497 $theme = gutenberg_experimental_global_styles_normalize_schema( $theme );
498 $user = gutenberg_experimental_global_styles_normalize_schema( $user );
499 $result = gutenberg_experimental_global_styles_normalize_schema( array() );
500
501 foreach ( array_keys( $core ) as $block_name ) {
502 foreach ( array( 'presets', 'styles', 'features' ) as $subtree ) {
503 $result[ $block_name ][ $subtree ] = array_merge(
504 $core[ $block_name ][ $subtree ],
505 $theme[ $block_name ][ $subtree ],
506 $user[ $block_name ][ $subtree ]
507 );
508 }
509 }
510
511 return $result;
512 }
513
514 /**
515 * Given a tree, it normalizes it to the expected schema.
516 *
517 * @param array $tree Source tree to normalize.
518 *
519 * @return array Normalized tree.
520 */
521 function gutenberg_experimental_global_styles_normalize_schema( $tree ) {
522 $block_schema = array(
523 'styles' => array(),
524 'features' => array(),
525 'presets' => array(),
526 );
527
528 $normalized_tree = array();
529 $block_data = gutenberg_experimental_global_styles_get_block_data();
530 foreach ( array_keys( $block_data ) as $block_name ) {
531 $normalized_tree[ $block_name ] = $block_schema;
532 }
533
534 $tree = array_merge_recursive(
535 $normalized_tree,
536 $tree
537 );
538
539 return $tree;
540 }
541
542 /**
543 * Returns the stylesheet resulting of merging
544 * core's, theme's, and user's origins.
545 *
546 * @return string
547 */
548 function gutenberg_experimental_global_styles_get_stylesheet() {
549 $gs_merged = array();
550 $gs_core = gutenberg_experimental_global_styles_get_core();
551 $gs_theme = gutenberg_experimental_global_styles_get_theme();
552 $gs_user = gutenberg_experimental_global_styles_get_user();
553
554 $gs_merged = gutenberg_experimental_global_styles_merge_trees( $gs_core, $gs_theme, $gs_user );
555
556 $stylesheet = gutenberg_experimental_global_styles_resolver( $gs_merged );
557 if ( empty( $stylesheet ) ) {
558 return;
559 }
560 return $stylesheet;
561 }
562
563 /**
564 * Fetches the preferences for each origin (core, theme, user)
565 * and enqueues the resulting stylesheet.
566 */
567 function gutenberg_experimental_global_styles_enqueue_assets() {
568
569 $stylesheet = gutenberg_experimental_global_styles_get_stylesheet();
570
571 wp_register_style( 'global-styles', false, array(), true, true );
572 wp_add_inline_style( 'global-styles', $stylesheet );
573 wp_enqueue_style( 'global-styles' );
574 }
575
576 /**
577 * Adds the necessary data for the Global Styles client UI to the block settings.
578 *
579 * @param array $settings Existing block editor settings.
580 * @return array New block editor settings
581 */
582 function gutenberg_experimental_global_styles_settings( $settings ) {
583
584 if ( gutenberg_experimental_global_styles_has_theme_json_support() ) {
585 $settings['__experimentalGlobalStylesUserEntityId'] = gutenberg_experimental_global_styles_get_user_cpt_id();
586
587 $global_styles = gutenberg_experimental_global_styles_merge_trees(
588 gutenberg_experimental_global_styles_get_core(),
589 gutenberg_experimental_global_styles_get_theme()
590 );
591
592 $settings['__experimentalGlobalStylesBase'] = $global_styles;
593 }
594
595 // Add the styles for the editor via the settings
596 // so they get processed as if they were added via add_editor_styles:
597 // they will get the editor wrapper class.
598 $settings['styles'][] = array( 'css' => gutenberg_experimental_global_styles_get_stylesheet() );
599
600 return $settings;
601 }
602
603 /**
604 * Registers a Custom Post Type to store the user's origin config.
605 */
606 function gutenberg_experimental_global_styles_register_cpt() {
607 if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) {
608 return;
609 }
610
611 $args = array(
612 'label' => __( 'Global Styles', 'gutenberg' ),
613 'description' => 'CPT to store user design tokens',
614 'public' => false,
615 'show_ui' => false,
616 'show_in_rest' => true,
617 'rest_base' => '__experimental/global-styles',
618 'capabilities' => array(
619 'read' => 'edit_theme_options',
620 'create_posts' => 'edit_theme_options',
621 'edit_posts' => 'edit_theme_options',
622 'edit_published_posts' => 'edit_theme_options',
623 'delete_published_posts' => 'edit_theme_options',
624 'edit_others_posts' => 'edit_theme_options',
625 'delete_others_posts' => 'edit_theme_options',
626 ),
627 'map_meta_cap' => true,
628 'supports' => array(
629 'editor',
630 'revisions',
631 ),
632 );
633 register_post_type( 'wp_global_styles', $args );
634 }
635
636 add_action( 'init', 'gutenberg_experimental_global_styles_register_cpt' );
637 add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings' );
638 add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' );
639