PluginProbe
Gutenberg / 9.2.2
Gutenberg v9.2.2
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 7.4.0 All 402 releases
gutenberg / lib / global-styles.php

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

980 lines 32.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 * It also transforms camelCase names into kebab-case
23 * and substitutes '/' by '-'.
24 *
25 * This is thought to be useful to generate
26 * CSS Custom Properties from a tree,
27 * although there's nothing in the implementation
28 * of this function that requires that format.
29 *
30 * For example, assuming the given prefix is '--wp'
31 * and the token is '--', for this input tree:
32 *
33 * {
34 * 'some/property': 'value',
35 * 'nestedProperty': {
36 * 'sub-property': 'value'
37 * }
38 * }
39 *
40 * it'll return this output:
41 *
42 * {
43 * '--wp--some-property': 'value',
44 * '--wp--nested-property--sub-property': 'value'
45 * }
46 *
47 * @param array $tree Input tree to process.
48 * @param string $prefix Prefix to prepend to each variable. '' by default.
49 * @param string $token Token to use between levels. '--' by default.
50 *
51 * @return array The flattened tree.
52 */
53 function gutenberg_experimental_global_styles_get_css_vars( $tree, $prefix = '', $token = '--' ) {
54 $result = array();
55 foreach ( $tree as $property => $value ) {
56 $new_key = $prefix . str_replace(
57 '/',
58 '-',
59 strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
60 );
61
62 if ( is_array( $value ) ) {
63 $new_prefix = $new_key . $token;
64 $result = array_merge(
65 $result,
66 gutenberg_experimental_global_styles_get_css_vars( $value, $new_prefix, $token )
67 );
68 } else {
69 $result[ $new_key ] = $value;
70 }
71 }
72 return $result;
73 }
74
75 /**
76 * Processes a file that adheres to the theme.json
77 * schema and returns an array with its contents,
78 * or a void array if none found.
79 *
80 * @param string $file_path Path to file.
81 * @return array Contents that adhere to the theme.json schema.
82 */
83 function gutenberg_experimental_global_styles_get_from_file( $file_path ) {
84 $config = array();
85 if ( file_exists( $file_path ) ) {
86 $decoded_file = json_decode(
87 file_get_contents( $file_path ),
88 true
89 );
90
91 $json_decoding_error = json_last_error();
92 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
93 error_log( 'Error when decoding file schema: ' . json_last_error_msg() );
94 return $config;
95 }
96
97 if ( is_array( $decoded_file ) ) {
98 $config = $decoded_file;
99 }
100 }
101 return $config;
102 }
103
104 /**
105 * Returns the user's origin config.
106 *
107 * @return array Config that adheres to the theme.json schema.
108 */
109 function gutenberg_experimental_global_styles_get_user() {
110 $config = array();
111 $user_cpt = gutenberg_experimental_global_styles_get_user_cpt();
112 if ( array_key_exists( 'post_content', $user_cpt ) ) {
113 $decoded_data = json_decode( $user_cpt['post_content'], true );
114
115 $json_decoding_error = json_last_error();
116 if ( JSON_ERROR_NONE !== $json_decoding_error ) {
117 error_log( 'Error when decoding user schema: ' . json_last_error_msg() );
118 return $config;
119 }
120
121 if ( is_array( $decoded_data ) ) {
122 $config = $decoded_data;
123 }
124 }
125
126 return $config;
127 }
128
129 /**
130 * Returns the CPT that contains the user's origin config
131 * for the current theme or a void array if none found.
132 *
133 * It can also create and return a new draft CPT.
134 *
135 * @param bool $should_create_cpt Whether a new CPT should be created if no one was found.
136 * False by default.
137 * @param array $post_status_filter Filter CPT by post status.
138 * ['publish'] by default, so it only fetches published posts.
139 * @return array Custom Post Type for the user's origin config.
140 */
141 function gutenberg_experimental_global_styles_get_user_cpt( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) {
142 $user_cpt = array();
143 $post_type_filter = 'wp_global_styles';
144 $post_name_filter = 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() );
145 $recent_posts = wp_get_recent_posts(
146 array(
147 'numberposts' => 1,
148 'orderby' => 'date',
149 'order' => 'desc',
150 'post_type' => $post_type_filter,
151 'post_status' => $post_status_filter,
152 'name' => $post_name_filter,
153 )
154 );
155
156 if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) {
157 $user_cpt = $recent_posts[0];
158 } elseif ( $should_create_cpt ) {
159 $cpt_post_id = wp_insert_post(
160 array(
161 'post_content' => '{}',
162 'post_status' => 'publish',
163 'post_type' => $post_type_filter,
164 'post_name' => $post_name_filter,
165 ),
166 true
167 );
168 $user_cpt = get_post( $cpt_post_id, ARRAY_A );
169 }
170
171 return $user_cpt;
172 }
173
174 /**
175 * Returns the post ID of the CPT containing the user's origin config.
176 *
177 * @return integer
178 */
179 function gutenberg_experimental_global_styles_get_user_cpt_id() {
180 $user_cpt_id = null;
181 $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( true );
182 if ( array_key_exists( 'ID', $user_cpt ) ) {
183 $user_cpt_id = $user_cpt['ID'];
184 }
185 return $user_cpt_id;
186 }
187
188 /**
189 * Return core's origin config.
190 *
191 * @return array Config that adheres to the theme.json schema.
192 */
193 function gutenberg_experimental_global_styles_get_core() {
194 $config = gutenberg_experimental_global_styles_get_from_file(
195 __DIR__ . '/experimental-default-theme.json'
196 );
197 // Start i18n logic to remove when JSON i18 strings are extracted.
198 $default_colors_i18n = array(
199 'black' => __( 'Black', 'gutenberg' ),
200 'cyan-bluish-gray' => __( 'Cyan bluish gray', 'gutenberg' ),
201 'white' => __( 'White', 'gutenberg' ),
202 'pale-pink' => __( 'Pale pink', 'gutenberg' ),
203 'vivid-red' => __( 'Vivid red', 'gutenberg' ),
204 'luminous-vivid-orange' => __( 'Luminous vivid orange', 'gutenberg' ),
205 'luminous-vivid-amber' => __( 'Luminous vivid amber', 'gutenberg' ),
206 'light-green-cyan' => __( 'Light green cyan', 'gutenberg' ),
207 'vivid-green-cyan' => __( 'Vivid green cyan', 'gutenberg' ),
208 'pale-cyan-blue' => __( 'Pale cyan blue', 'gutenberg' ),
209 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ),
210 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ),
211 );
212
213 if ( ! empty( $config['global']['settings']['color']['palette'] ) ) {
214 foreach ( $config['global']['settings']['color']['palette'] as &$color ) {
215 $color['name'] = $default_colors_i18n[ $color['slug'] ];
216 }
217 }
218
219 $default_gradients_i18n = array(
220 'vivid-cyan-blue-to-vivid-purple' => __( 'Vivid cyan blue to vivid purple', 'gutenberg' ),
221 'light-green-cyan-to-vivid-green-cyan' => __( 'Light green cyan to vivid green cyan', 'gutenberg' ),
222 'luminous-vivid-amber-to-luminous-vivid-orange' => __( 'Luminous vivid amber to luminous vivid orange', 'gutenberg' ),
223 'luminous-vivid-orange-to-vivid-red' => __( 'Luminous vivid orange to vivid red', 'gutenberg' ),
224 'very-light-gray-to-cyan-bluish-gray' => __( 'Very light gray to cyan bluish gray', 'gutenberg' ),
225 'cool-to-warm-spectrum' => __( 'Cool to warm spectrum', 'gutenberg' ),
226 'blush-light-purple' => __( 'Blush light purple', 'gutenberg' ),
227 'blush-bordeaux' => __( 'Blush bordeaux', 'gutenberg' ),
228 'luminous-dusk' => __( 'Luminous dusk', 'gutenberg' ),
229 'pale-ocean' => __( 'Pale ocean', 'gutenberg' ),
230 'electric-grass' => __( 'Electric grass', 'gutenberg' ),
231 'midnight' => __( 'Midnight', 'gutenberg' ),
232 );
233
234 if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) {
235 foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) {
236 $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ];
237 }
238 }
239
240 $default_font_sizes_i18n = array(
241 'small' => __( 'Small', 'gutenberg' ),
242 'normal' => __( 'Normal', 'gutenberg' ),
243 'medium' => __( 'Medium', 'gutenberg' ),
244 'large' => __( 'Large', 'gutenberg' ),
245 'huge' => __( 'Huge', 'gutenberg' ),
246 );
247
248 if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) {
249 foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) {
250 $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ];
251 }
252 }
253 // End i18n logic to remove when JSON i18 strings are extracted.
254 return $config;
255 }
256
257 /**
258 * Returns the theme presets registered via add_theme_support, if any.
259 *
260 * @return array Config that adheres to the theme.json schema.
261 */
262 function gutenberg_experimental_global_styles_get_theme_support_settings() {
263 $theme_settings = array();
264 $theme_settings['global'] = array();
265 $theme_settings['global']['settings'] = array();
266
267 // Deprecated theme supports.
268 if ( get_theme_support( 'disable-custom-colors' ) ) {
269 if ( ! isset( $theme_settings['global']['settings']['color'] ) ) {
270 $theme_settings['global']['settings']['color'] = array();
271 }
272 $theme_settings['global']['settings']['color']['custom'] = false;
273 }
274 if ( get_theme_support( 'disable-custom-gradients' ) ) {
275 if ( ! isset( $theme_settings['global']['settings']['color'] ) ) {
276 $theme_settings['global']['settings']['color'] = array();
277 }
278 $theme_settings['global']['settings']['color']['customGradient'] = false;
279 }
280 if ( get_theme_support( 'disable-custom-font-sizes' ) ) {
281 if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) {
282 $theme_settings['global']['settings']['typography'] = array();
283 }
284 $theme_settings['global']['settings']['typography']['customFontSize'] = false;
285 }
286 if ( get_theme_support( 'custom-line-height' ) ) {
287 if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) {
288 $theme_settings['global']['settings']['typography'] = array();
289 }
290 $theme_settings['global']['settings']['typography']['customLineHeight'] = true;
291 }
292 if ( get_theme_support( 'custom-spacing' ) ) {
293 if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) {
294 $theme_settings['global']['settings']['spacing'] = array();
295 }
296 $theme_settings['global']['settings']['spacing']['custom'] = true;
297 }
298 if ( get_theme_support( 'experimental-link-color' ) ) {
299 if ( ! isset( $theme_settings['global']['settings']['color'] ) ) {
300 $theme_settings['global']['settings']['color'] = array();
301 }
302 $theme_settings['global']['settings']['color']['link'] = true;
303 }
304
305 $custom_units_theme_support = get_theme_support( 'custom-units' );
306 if ( $custom_units_theme_support ) {
307 if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) {
308 $theme_settings['global']['settings']['spacing'] = array();
309 }
310 $theme_settings['global']['settings']['spacing'] ['units'] = true === $custom_units_theme_support ? array( 'px', 'em', 'rem', 'vh', 'vw' ) : $custom_units_theme_support;
311 }
312
313 $theme_colors = get_theme_support( 'editor-color-palette' );
314 if ( ! empty( $theme_colors[0] ) ) {
315 if ( ! isset( $theme_settings['global']['settings']['color'] ) ) {
316 $theme_settings['global']['settings']['color'] = array();
317 }
318 $theme_settings['global']['settings']['color']['palette'] = array();
319 $theme_settings['global']['settings']['color']['palette'] = $theme_colors[0];
320 }
321
322 $theme_gradients = get_theme_support( 'editor-gradient-presets' );
323 if ( ! empty( $theme_gradients[0] ) ) {
324 if ( ! isset( $theme_settings['global']['settings']['color'] ) ) {
325 $theme_settings['global']['settings']['color'] = array();
326 }
327 $theme_settings['global']['settings']['color']['gradients'] = array();
328 $theme_settings['global']['settings']['color']['gradients'] = $theme_gradients[0];
329 }
330
331 $theme_font_sizes = get_theme_support( 'editor-font-sizes' );
332 if ( ! empty( $theme_font_sizes[0] ) ) {
333 if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) {
334 $theme_settings['global']['settings']['typography'] = array();
335 }
336 $theme_settings['global']['settings']['typography']['fontSizes'] = array();
337 $theme_settings['global']['settings']['typography']['fontSizes'] = $theme_font_sizes[0];
338 }
339
340 return $theme_settings;
341 }
342
343 /**
344 * Returns the theme's origin config.
345 *
346 * It also fetches the existing presets the theme declared via add_theme_support
347 * and uses them if the theme hasn't declared any via theme.json.
348 *
349 * @return array Config that adheres to the theme.json schema.
350 */
351 function gutenberg_experimental_global_styles_get_theme() {
352 $theme_support_settings = gutenberg_experimental_global_styles_get_theme_support_settings();
353 $theme_config = gutenberg_experimental_global_styles_get_from_file(
354 locate_template( 'experimental-theme.json' )
355 );
356
357 /*
358 * We want the presets declared in theme.json
359 * to take precedence over the ones declared via add_theme_support.
360 *
361 * Note that merging happens at the preset category level. Example:
362 *
363 * - if the theme declares a color palette via add_theme_support &
364 * a set of font sizes via theme.json, both will be included in the output.
365 *
366 * - if the theme declares a color palette both via add_theme_support &
367 * via theme.json, the later takes precedence.
368 *
369 */
370 $theme_config = gutenberg_experimental_global_styles_merge_trees(
371 $theme_support_settings,
372 $theme_config
373 );
374
375 return $theme_config;
376 }
377
378 /**
379 * Convert style property to its CSS name.
380 *
381 * @param string $style_property Style property name.
382 * @return string CSS property name.
383 */
384 function gutenberg_experimental_global_styles_get_css_property( $style_property ) {
385 switch ( $style_property ) {
386 case 'backgroundColor':
387 return 'background-color';
388 case 'fontSize':
389 return 'font-size';
390 case 'lineHeight':
391 return 'line-height';
392 default:
393 return $style_property;
394 }
395 }
396
397 /**
398 * Return how the style property is structured.
399 *
400 * @return array Style property structure.
401 */
402 function gutenberg_experimental_global_styles_get_style_property() {
403 return array(
404 '--wp--style--color--link' => array( 'color', 'link' ),
405 'background' => array( 'color', 'gradient' ),
406 'backgroundColor' => array( 'color', 'background' ),
407 'color' => array( 'color', 'text' ),
408 'fontSize' => array( 'typography', 'fontSize' ),
409 'lineHeight' => array( 'typography', 'lineHeight' ),
410 );
411 }
412
413 /**
414 * Return how the support keys are structured.
415 *
416 * @return array Support keys structure.
417 */
418 function gutenberg_experimental_global_styles_get_support_keys() {
419 return array(
420 '--wp--style--color--link' => array( 'color', 'linkColor' ),
421 'background' => array( 'color', 'gradients' ),
422 'backgroundColor' => array( 'color' ),
423 'color' => array( 'color' ),
424 'fontSize' => array( 'fontSize' ),
425 'lineHeight' => array( 'lineHeight' ),
426 );
427 }
428
429 /**
430 * Returns how the presets css variables are structured on the global styles data.
431 *
432 * @return array Presets structure
433 */
434 function gutenberg_experimental_global_styles_get_presets_structure() {
435 return array(
436 'color' => array(
437 'path' => array( 'color', 'palette' ),
438 'key' => 'color',
439 ),
440 'gradient' => array(
441 'path' => array( 'color', 'gradients' ),
442 'key' => 'gradient',
443 ),
444 'fontSize' => array(
445 'path' => array( 'typography', 'fontSizes' ),
446 'key' => 'size',
447 ),
448 );
449 }
450
451 /**
452 * Returns the style features a particular block supports.
453 *
454 * @param array $supports The block supports array.
455 *
456 * @return array Style features supported by the block.
457 */
458 function gutenberg_experimental_global_styles_get_supported_styles( $supports ) {
459 $support_keys = gutenberg_experimental_global_styles_get_support_keys();
460 $supported_features = array();
461 foreach ( $support_keys as $key => $path ) {
462 if ( gutenberg_experimental_get( $supports, $path ) ) {
463 $supported_features[] = $key;
464 }
465 }
466
467 return $supported_features;
468 }
469
470 /**
471 * Retrieves the block data (selector/supports).
472 *
473 * @return array
474 */
475 function gutenberg_experimental_global_styles_get_block_data() {
476 $block_data = array();
477
478 $registry = WP_Block_Type_Registry::get_instance();
479 $blocks = array_merge(
480 $registry->get_all_registered(),
481 array(
482 'global' => new WP_Block_Type(
483 'global',
484 array(
485 'supports' => array(
486 '__experimentalSelector' => ':root',
487 'fontSize' => true,
488 'color' => array(
489 'linkColor' => true,
490 'gradients' => true,
491 ),
492 ),
493 )
494 ),
495 )
496 );
497 foreach ( $blocks as $block_name => $block_type ) {
498 if ( ! property_exists( $block_type, 'supports' ) || empty( $block_type->supports ) || ! is_array( $block_type->supports ) ) {
499 continue;
500 }
501
502 $supports = gutenberg_experimental_global_styles_get_supported_styles( $block_type->supports );
503
504 /*
505 * Assign the selector for the block.
506 *
507 * Some blocks can declare multiple selectors:
508 *
509 * - core/heading represents the H1-H6 HTML elements
510 * - core/list represents the UL and OL HTML elements
511 * - core/group is meant to represent DIV and other HTML elements
512 *
513 * Some other blocks don't provide a selector,
514 * so we generate a class for them based on their name:
515 *
516 * - 'core/group' => '.wp-block-group'
517 * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name'
518 *
519 * Note that, for core blocks, we don't add the `core/` prefix to its class name.
520 * This is for historical reasons, as they come with a class without that infix.
521 *
522 */
523 if (
524 isset( $block_type->supports['__experimentalSelector'] ) &&
525 is_string( $block_type->supports['__experimentalSelector'] )
526 ) {
527 $block_data[ $block_name ] = array(
528 'selector' => $block_type->supports['__experimentalSelector'],
529 'supports' => $supports,
530 'blockName' => $block_name,
531 );
532 } elseif (
533 isset( $block_type->supports['__experimentalSelector'] ) &&
534 is_array( $block_type->supports['__experimentalSelector'] )
535 ) {
536 foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector ) {
537 $block_data[ $key ] = array(
538 'selector' => $selector,
539 'supports' => $supports,
540 'blockName' => $block_name,
541 );
542 }
543 } else {
544 $block_data[ $block_name ] = array(
545 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ),
546 'supports' => $supports,
547 'blockName' => $block_name,
548 );
549 }
550 }
551
552 return $block_data;
553 }
554
555 /**
556 * Given an array contain the styles shape returns the css for this styles.
557 * A similar function exists on the client at /packages/block-editor/src/hooks/style.js.
558 *
559 * @param array $styles Array containing the styles shape from global styles.
560 *
561 * @return array Containing a set of css rules.
562 */
563 function gutenberg_experimental_global_styles_flatten_styles_tree( $styles ) {
564 $mappings = gutenberg_experimental_global_styles_get_style_property();
565
566 $result = array();
567 foreach ( $mappings as $key => $path ) {
568 $value = gutenberg_experimental_get( $styles, $path, null );
569 if ( null !== $value ) {
570 $result[ $key ] = $value;
571 }
572 }
573 return $result;
574
575 }
576
577 /**
578 * Given a selector for a block, and the settings of the block, returns a string
579 * with the stylesheet of the preset classes required for that block.
580 *
581 * @param string $selector String with the CSS selector for the block.
582 * @param array $settings Array containing the settings of the block.
583 *
584 * @return string Stylesheet with the preset classes.
585 */
586 function gutenberg_experimental_global_styles_get_preset_classes( $selector, $settings ) {
587 if ( empty( $settings ) || empty( $selector ) ) {
588 return '';
589 }
590
591 $stylesheet = '';
592 $class_prefix = 'has';
593 $classes_structure = array(
594 'color' => array(
595 'path' => array( 'color', 'palette' ),
596 'key' => 'color',
597 'property' => 'color',
598 ),
599 'background-color' => array(
600 'path' => array( 'color', 'palette' ),
601 'key' => 'color',
602 'property' => 'background-color',
603 ),
604 'gradient-background' => array(
605 'path' => array( 'color', 'gradients' ),
606 'key' => 'gradient',
607 'property' => 'background',
608 ),
609 'font-size' => array(
610 'path' => array( 'typography', 'fontSizes' ),
611 'key' => 'size',
612 'property' => 'font-size',
613 ),
614 );
615
616 foreach ( $classes_structure as $class_suffix => $preset_structure ) {
617 $path = $preset_structure['path'];
618 $presets = gutenberg_experimental_get( $settings, $path );
619
620 if ( empty( $presets ) ) {
621 continue;
622 }
623
624 $key = $preset_structure['key'];
625 $property = $preset_structure['property'];
626
627 foreach ( $presets as $preset ) {
628 $slug = $preset['slug'];
629 $value = $preset[ $key ];
630
631 $class_to_use = ".$class_prefix-$slug-$class_suffix";
632 $selector_to_use = '';
633 if ( ':root' === $selector ) {
634 $selector_to_use = $class_to_use;
635 } else {
636 $selector_to_use = "$selector$class_to_use";
637 }
638 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
639 $stylesheet .= "$selector_to_use {\n\t$property: $value;\n}\n";
640 } else {
641 $stylesheet .= $selector_to_use . '{' . "$property:$value;}\n";
642 }
643 }
644 }
645 return $stylesheet;
646 }
647
648 /**
649 * Takes a tree adhering to the theme.json schema and generates
650 * the corresponding stylesheet.
651 *
652 * @param array $tree Input tree.
653 *
654 * @return string Stylesheet.
655 */
656 function gutenberg_experimental_global_styles_get_stylesheet( $tree ) {
657 $stylesheet = '';
658 $block_data = gutenberg_experimental_global_styles_get_block_data();
659 foreach ( array_keys( $tree ) as $block_name ) {
660 if (
661 ! array_key_exists( $block_name, $block_data ) ||
662 ! array_key_exists( 'selector', $block_data[ $block_name ] ) ||
663 ! array_key_exists( 'supports', $block_data[ $block_name ] )
664 ) {
665 // Skip blocks that haven't declared support,
666 // because we don't know to process them.
667 continue;
668 }
669
670 // Create the CSS Custom Properties for the presets.
671 $computed_presets = array();
672 $presets_structure = gutenberg_experimental_global_styles_get_presets_structure();
673 foreach ( $presets_structure as $token => $preset_meta ) {
674 $block_preset = gutenberg_experimental_get( $tree[ $block_name ]['settings'], $preset_meta['path'] );
675 if ( ! empty( $block_preset ) ) {
676 $computed_presets[ $token ] = array();
677 foreach ( $block_preset as $preset_value ) {
678 $computed_presets[ $token ][ $preset_value['slug'] ] = $preset_value[ $preset_meta['key'] ];
679 }
680 }
681 }
682 $token = '--';
683 $preset_prefix = '--wp--preset' . $token;
684 $preset_variables = gutenberg_experimental_global_styles_get_css_vars( $computed_presets, $preset_prefix, $token );
685
686 // Create the CSS Custom Properties that are specific to the theme.
687 $computed_theme_props = gutenberg_experimental_get( $tree[ $block_name ]['settings'], array( 'custom' ) );
688 $theme_props_prefix = '--wp--custom' . $token;
689 $theme_variables = gutenberg_experimental_global_styles_get_css_vars(
690 $computed_theme_props,
691 $theme_props_prefix,
692 $token
693 );
694
695 $stylesheet .= gutenberg_experimental_global_styles_resolver_styles(
696 $block_data[ $block_name ]['selector'],
697 $block_data[ $block_name ]['supports'],
698 array_merge(
699 gutenberg_experimental_global_styles_flatten_styles_tree( $tree[ $block_name ]['styles'] ),
700 $preset_variables,
701 $theme_variables
702 )
703 );
704
705 $stylesheet .= gutenberg_experimental_global_styles_get_preset_classes( $block_data[ $block_name ]['selector'], $tree[ $block_name ]['settings'] );
706 }
707
708 if ( gutenberg_experimental_global_styles_has_theme_json_support() ) {
709 // To support all themes, we added in the block-library stylesheet
710 // a style rule such as .has-link-color a { color: var(--wp--style--color--link, #00e); }
711 // so that existing link colors themes used didn't break.
712 // We add this here to make it work for themes that opt-in to theme.json
713 // In the future, we may do this differently.
714 $stylesheet .= 'a{color:var(--wp--style--color--link, #00e);}';
715 }
716
717 return $stylesheet;
718 }
719
720 /**
721 * Generates CSS declarations for a block.
722 *
723 * @param string $block_selector CSS selector for the block.
724 * @param array $block_supports A list of properties supported by the block.
725 * @param array $block_styles The list of properties/values to be converted to CSS.
726 *
727 * @return string The corresponding CSS rule.
728 */
729 function gutenberg_experimental_global_styles_resolver_styles( $block_selector, $block_supports, $block_styles ) {
730 $css_property = '';
731 $css_rule = '';
732 $css_declarations = '';
733
734 foreach ( $block_styles as $property => $value ) {
735 // Only convert to CSS:
736 //
737 // 1) The style attributes the block has declared support for.
738 // 2) Any CSS custom property attached to the node.
739 if (
740 in_array( $property, $block_supports, true ) ||
741 strstr( $property, '--' )
742 ) {
743 $css_property = gutenberg_experimental_global_styles_get_css_property( $property );
744
745 // Add whitespace if SCRIPT_DEBUG is defined and set to true.
746 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
747 $css_declarations .= "\t" . $css_property . ': ' . $value . ";\n";
748 } else {
749 $css_declarations .= $css_property . ':' . $value . ';';
750 }
751 }
752 }
753
754 if ( '' !== $css_declarations ) {
755
756 // Add whitespace if SCRIPT_DEBUG is defined and set to true.
757 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
758 $css_rule .= $block_selector . " {\n";
759 $css_rule .= $css_declarations;
760 $css_rule .= "}\n";
761 } else {
762 $css_rule .= $block_selector . '{' . $css_declarations . '}';
763 }
764 }
765
766 return $css_rule;
767 }
768
769 /**
770 * Helper function that merges trees that adhere to the theme.json schema.
771 *
772 * @param array $core Core origin.
773 * @param array $theme Theme origin.
774 * @param array $user User origin. An empty array by default.
775 *
776 * @return array The merged result.
777 */
778 function gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user = array() ) {
779 $core = gutenberg_experimental_global_styles_normalize_schema( $core );
780 $theme = gutenberg_experimental_global_styles_normalize_schema( $theme );
781 $user = gutenberg_experimental_global_styles_normalize_schema( $user );
782 $result = gutenberg_experimental_global_styles_normalize_schema( array() );
783
784 foreach ( array_keys( $core ) as $block_name ) {
785 foreach ( array_keys( $core[ $block_name ]['settings'] ) as $subtree ) {
786 $result[ $block_name ]['settings'][ $subtree ] = array_merge(
787 $core[ $block_name ]['settings'][ $subtree ],
788 $theme[ $block_name ]['settings'][ $subtree ],
789 $user[ $block_name ]['settings'][ $subtree ]
790 );
791 }
792 foreach ( array_keys( $core[ $block_name ]['styles'] ) as $subtree ) {
793 $result[ $block_name ]['styles'][ $subtree ] = array_merge(
794 $core[ $block_name ]['styles'][ $subtree ],
795 $theme[ $block_name ]['styles'][ $subtree ],
796 $user[ $block_name ]['styles'][ $subtree ]
797 );
798 }
799 }
800
801 return $result;
802 }
803
804 /**
805 * Given a tree, it normalizes it to the expected schema.
806 *
807 * @param array $tree Source tree to normalize.
808 *
809 * @return array Normalized tree.
810 */
811 function gutenberg_experimental_global_styles_normalize_schema( $tree ) {
812 $block_schema = array(
813 'styles' => array(
814 'typography' => array(),
815 'color' => array(),
816 ),
817 'settings' => array(
818 'color' => array(),
819 'custom' => array(),
820 'typography' => array(),
821 'spacing' => array(),
822 ),
823 );
824
825 $normalized_tree = array();
826 $block_data = gutenberg_experimental_global_styles_get_block_data();
827 foreach ( array_keys( $block_data ) as $block_name ) {
828 $normalized_tree[ $block_name ] = $block_schema;
829 }
830
831 $tree = array_merge_recursive(
832 $normalized_tree,
833 $tree
834 );
835
836 return $tree;
837 }
838
839 /**
840 * Takes data from the different origins (core, theme, and user)
841 * and returns the merged result.
842 *
843 * @return array Merged trees
844 */
845 function gutenberg_experimental_global_styles_get_merged_origins() {
846 $core = gutenberg_experimental_global_styles_get_core();
847 $theme = gutenberg_experimental_global_styles_get_theme();
848 $user = gutenberg_experimental_global_styles_get_user();
849
850 return gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user );
851 }
852
853 /**
854 * Fetches the preferences for each origin (core, theme, user)
855 * and enqueues the resulting stylesheet.
856 */
857 function gutenberg_experimental_global_styles_enqueue_assets() {
858 $merged = gutenberg_experimental_global_styles_get_merged_origins();
859 $stylesheet = gutenberg_experimental_global_styles_get_stylesheet( $merged );
860 if ( empty( $stylesheet ) ) {
861 return;
862 }
863
864 wp_register_style( 'global-styles', false, array(), true, true );
865 wp_add_inline_style( 'global-styles', $stylesheet );
866 wp_enqueue_style( 'global-styles' );
867 }
868
869 /**
870 * Returns the default config for editor features,
871 * or an empty array if none found.
872 *
873 * @param array $config Config to extract values from.
874 * @return array Default features config for the editor.
875 */
876 function gutenberg_experimental_global_styles_get_editor_settings( $config ) {
877 $settings = array();
878 foreach ( array_keys( $config ) as $context ) {
879 if (
880 empty( $config[ $context ]['settings'] ) ||
881 ! is_array( $config[ $context ]['settings'] )
882 ) {
883 $settings[ $context ] = array();
884 } else {
885 $settings[ $context ] = $config[ $context ]['settings'];
886 }
887 }
888 return $settings;
889 }
890
891 /**
892 * Adds the necessary data for the Global Styles client UI to the block settings.
893 *
894 * @param array $settings Existing block editor settings.
895 * @return array New block editor settings
896 */
897 function gutenberg_experimental_global_styles_settings( $settings ) {
898 $merged = gutenberg_experimental_global_styles_get_merged_origins();
899
900 // STEP 1: ADD FEATURES
901 // These need to be added to settings always.
902 // We also need to unset the deprecated settings defined by core.
903 $settings['__experimentalFeatures'] = gutenberg_experimental_global_styles_get_editor_settings( $merged );
904
905 unset( $settings['colors'] );
906 unset( $settings['gradients'] );
907 unset( $settings['fontSizes'] );
908 unset( $settings['disableCustomColors'] );
909 unset( $settings['disableCustomGradients'] );
910 unset( $settings['disableCustomFontSizes'] );
911 unset( $settings['enableCustomLineHeight'] );
912 unset( $settings['enableCustomUnits'] );
913
914 // STEP 2 - IF EDIT-SITE, ADD DATA REQUIRED FOR GLOBAL STYLES SIDEBAR
915 // The client needs some information to be able to access/update the user styles.
916 // We only do this if the theme has support for theme.json, though,
917 // as an indicator that the theme will know how to combine this with its stylesheet.
918 $screen = get_current_screen();
919 if (
920 ! empty( $screen ) &&
921 function_exists( 'gutenberg_is_edit_site_page' ) &&
922 gutenberg_is_edit_site_page( $screen->id ) &&
923 gutenberg_experimental_global_styles_has_theme_json_support()
924 ) {
925 $settings['__experimentalGlobalStylesUserEntityId'] = gutenberg_experimental_global_styles_get_user_cpt_id();
926 $settings['__experimentalGlobalStylesContexts'] = gutenberg_experimental_global_styles_get_block_data();
927 $settings['__experimentalGlobalStylesBaseStyles'] = gutenberg_experimental_global_styles_merge_trees(
928 gutenberg_experimental_global_styles_get_core(),
929 gutenberg_experimental_global_styles_get_theme()
930 );
931 } else {
932 // STEP 3 - OTHERWISE, ADD STYLES
933 //
934 // If we are in a block editor context, but not in edit-site,
935 // we need to add the styles via the settings. This is because
936 // we want them processed as if they were added via add_editor_styles,
937 // which adds the editor wrapper class.
938 $settings['styles'][] = array( 'css' => gutenberg_experimental_global_styles_get_stylesheet( $merged ) );
939 }
940
941 return $settings;
942 }
943
944 /**
945 * Registers a Custom Post Type to store the user's origin config.
946 */
947 function gutenberg_experimental_global_styles_register_cpt() {
948 if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) {
949 return;
950 }
951
952 $args = array(
953 'label' => __( 'Global Styles', 'gutenberg' ),
954 'description' => 'CPT to store user design tokens',
955 'public' => false,
956 'show_ui' => false,
957 'show_in_rest' => true,
958 'rest_base' => '__experimental/global-styles',
959 'capabilities' => array(
960 'read' => 'edit_theme_options',
961 'create_posts' => 'edit_theme_options',
962 'edit_posts' => 'edit_theme_options',
963 'edit_published_posts' => 'edit_theme_options',
964 'delete_published_posts' => 'edit_theme_options',
965 'edit_others_posts' => 'edit_theme_options',
966 'delete_others_posts' => 'edit_theme_options',
967 ),
968 'map_meta_cap' => true,
969 'supports' => array(
970 'editor',
971 'revisions',
972 ),
973 );
974 register_post_type( 'wp_global_styles', $args );
975 }
976
977 add_action( 'init', 'gutenberg_experimental_global_styles_register_cpt' );
978 add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings' );
979 add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' );
980