PluginProbe
Gutenberg / 8.2.0
Gutenberg v8.2.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 7.4.0 All 402 releases
gutenberg / lib / global-styles.php

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

548 lines 16.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 if ( is_array( $decoded_file ) ) {
84 $config = $decoded_file;
85 }
86 }
87 return $config;
88 }
89
90 /**
91 * Returns the user's origin config.
92 *
93 * @return array Config that adheres to the theme.json schema.
94 */
95 function gutenberg_experimental_global_styles_get_user() {
96 $config = array();
97 $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( array( 'publish' ) );
98 if ( array_key_exists( 'post_content', $user_cpt ) ) {
99 $decoded_data = json_decode( $user_cpt['post_content'], true );
100 if ( is_array( $decoded_data ) ) {
101 $config = $decoded_data;
102 }
103 }
104
105 return $config;
106 }
107
108 /**
109 * Returns the CPT that contains the user's origin config
110 * for the current theme or a void array if none found.
111 *
112 * It can also create and return a new draft CPT.
113 *
114 * @param array $post_status_filter Filter CPT by post status.
115 * ['publish'] by default, so it only fetches published posts.
116 * @param bool $should_create_draft Whether a new draft should be created if no CPT was found.
117 * False by default.
118 * @return array Custom Post Type for the user's origin config.
119 */
120 function gutenberg_experimental_global_styles_get_user_cpt( $post_status_filter = array( 'publish' ), $should_create_draft = false ) {
121 $user_cpt = array();
122 $post_type_filter = 'wp_global_styles';
123 $post_name_filter = 'wp-global-styles-' . strtolower( wp_get_theme()->get( 'TextDomain' ) );
124 $recent_posts = wp_get_recent_posts(
125 array(
126 'numberposts' => 1,
127 'orderby' => 'date',
128 'order' => 'desc',
129 'post_type' => $post_type_filter,
130 'post_status' => $post_status_filter,
131 'name' => $post_name_filter,
132 )
133 );
134
135 if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) {
136 $user_cpt = $recent_posts[0];
137 } elseif ( $should_create_draft ) {
138 $cpt_post_id = wp_insert_post(
139 array(
140 'post_content' => '{}',
141 'post_status' => 'draft',
142 'post_type' => $post_type_filter,
143 'post_name' => $post_name_filter,
144 ),
145 true
146 );
147 $user_cpt = get_post( $cpt_post_id, ARRAY_A );
148 }
149
150 return $user_cpt;
151 }
152
153 /**
154 * Returns the post ID of the CPT containing the user's origin config.
155 *
156 * @return integer
157 */
158 function gutenberg_experimental_global_styles_get_user_cpt_id() {
159 $user_cpt_id = null;
160 $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( array( 'publish', 'draft' ), true );
161 if ( array_key_exists( 'ID', $user_cpt ) ) {
162 $user_cpt_id = $user_cpt['ID'];
163 }
164 return $user_cpt_id;
165 }
166
167 /**
168 * Return core's origin config.
169 *
170 * @return array Config that adheres to the theme.json schema.
171 */
172 function gutenberg_experimental_global_styles_get_core() {
173 $config = gutenberg_experimental_global_styles_get_from_file(
174 dirname( dirname( __FILE__ ) ) . '/experimental-default-global-styles.json'
175 );
176
177 return $config;
178 }
179
180 /**
181 * Returns the theme presets registered via add_theme_support, if any.
182 *
183 * @return array Config that adheres to the theme.json schema.
184 */
185 function gutenberg_experimental_global_styles_get_theme_presets() {
186 $theme_presets = array();
187
188 $theme_colors = get_theme_support( 'editor-color-palette' )[0];
189 if ( is_array( $theme_colors ) ) {
190 foreach ( $theme_colors as $color ) {
191 $theme_presets['global']['presets']['color'][] = array(
192 'slug' => $color['slug'],
193 'value' => $color['color'],
194 );
195 }
196 }
197
198 $theme_gradients = get_theme_support( 'editor-gradient-presets' )[0];
199 if ( is_array( $theme_gradients ) ) {
200 foreach ( $theme_gradients as $gradient ) {
201 $theme_presets['global']['presets']['gradient'][] = array(
202 'slug' => $gradient['slug'],
203 'value' => $gradient['gradient'],
204 );
205 }
206 }
207
208 $theme_font_sizes = get_theme_support( 'editor-font-sizes' )[0];
209 if ( is_array( $theme_font_sizes ) ) {
210 foreach ( $theme_font_sizes as $font_size ) {
211 $theme_presets['global']['presets']['font-size'][] = array(
212 'slug' => $font_size['slug'],
213 'value' => $font_size['size'],
214 );
215 }
216 }
217
218 return $theme_presets;
219 }
220
221 /**
222 * Returns the theme's origin config.
223 *
224 * It also fetches the existing presets the theme declared via add_theme_support
225 * and uses them if the theme hasn't declared any via theme.json.
226 *
227 * @return array Config that adheres to the theme.json schema.
228 */
229 function gutenberg_experimental_global_styles_get_theme() {
230 $theme_presets = gutenberg_experimental_global_styles_get_theme_presets();
231 $theme_config = gutenberg_experimental_global_styles_get_from_file(
232 locate_template( 'experimental-theme.json' )
233 );
234
235 /*
236 * We want the presets declared in theme.json
237 * to take precedence over the ones declared via add_theme_support.
238 *
239 * Note that merging happens at the preset category level. Example:
240 *
241 * - if the theme declares a color palette via add_theme_support &
242 * a set of font sizes via theme.json, both will be included in the output.
243 *
244 * - if the theme declares a color palette both via add_theme_support &
245 * via theme.json, the later takes precedence.
246 *
247 */
248 $theme_config = gutenberg_experimental_global_styles_merge_trees(
249 $theme_presets,
250 $theme_config
251 );
252
253 return $theme_config;
254 }
255
256 /**
257 * Retrieves the block data (selector/supports).
258 *
259 * @return array
260 */
261 function gutenberg_experimental_global_styles_get_block_data() {
262 // TODO: this data should be taken from the block registry.
263 //
264 // At the moment this array replicates the current capabilities
265 // declared by blocks via __experimentalLineHeight,
266 // __experimentalColor, and __experimentalFontSize.
267 $block_data = array(
268 'global' => array(
269 'selector' => ':root',
270 'supports' => array(), // By being blank, the 'global' section won't output any style yet.
271 ),
272 'core/paragraph' => array(
273 'selector' => 'p',
274 'supports' => array( 'line-height', 'font-size', 'color' ),
275 ),
276 'core/heading/h1' => array(
277 'selector' => 'h1',
278 'supports' => array( 'line-height', 'font-size', 'color' ),
279 ),
280 'core/heading/h2' => array(
281 'selector' => 'h2',
282 'supports' => array( 'line-height', 'font-size', 'color' ),
283 ),
284 'core/heading/h3' => array(
285 'selector' => 'h3',
286 'supports' => array( 'line-height', 'font-size', 'color' ),
287 ),
288 'core/heading/h4' => array(
289 'selector' => 'h4',
290 'supports' => array( 'line-height', 'font-size', 'color' ),
291 ),
292 'core/heading/h5' => array(
293 'selector' => 'h5',
294 'supports' => array( 'line-height', 'font-size', 'color' ),
295 ),
296 'core/heading/h6' => array(
297 'selector' => 'h6',
298 'supports' => array( 'line-height', 'font-size', 'color' ),
299 ),
300 'core/columns' => array(
301 'selector' => '.wp-block-columns',
302 'supports' => array( 'color' ),
303 ),
304 'core/group' => array(
305 'selector' => '.wp-block-group',
306 'supports' => array( 'color' ),
307 ),
308 'core/media-text' => array(
309 'selector' => '.wp-block-media-text',
310 'supports' => array( 'color' ),
311 ),
312 );
313
314 return $block_data;
315 }
316
317 /**
318 * Takes a tree adhering to the theme.json schema and generates
319 * the corresponding stylesheet.
320 *
321 * @param array $tree Input tree.
322 *
323 * @return string Stylesheet.
324 */
325 function gutenberg_experimental_global_styles_resolver( $tree ) {
326 $stylesheet = '';
327 $block_data = gutenberg_experimental_global_styles_get_block_data();
328 foreach ( array_keys( $tree ) as $block_name ) {
329 if (
330 ! array_key_exists( $block_name, $block_data ) ||
331 ! array_key_exists( 'selector', $block_data[ $block_name ] ) ||
332 ! array_key_exists( 'supports', $block_data[ $block_name ] )
333 ) {
334 // Skip blocks that haven't declared support,
335 // because we don't know to process them.
336 continue;
337 }
338
339 // Extract the relevant preset info before converting them to CSS Custom Properties.
340 foreach ( array_keys( $tree[ $block_name ]['presets'] ) as $preset_category ) {
341 $flattened_values = array();
342 foreach ( $tree[ $block_name ]['presets'][ $preset_category ] as $preset_value ) {
343 $flattened_values[ $preset_value['slug'] ] = $preset_value['value'];
344 }
345 $tree[ $block_name ]['presets'][ $preset_category ] = $flattened_values;
346 }
347
348 $token = '--';
349 $prefix = '--wp--preset' . $token;
350 $css_variables = gutenberg_experimental_global_styles_get_css_vars( $tree[ $block_name ]['presets'], $prefix, $token );
351
352 $stylesheet .= gutenberg_experimental_global_styles_resolver_styles(
353 $block_data[ $block_name ]['selector'],
354 $block_data[ $block_name ]['supports'],
355 array_merge( $tree[ $block_name ]['styles'], $css_variables )
356 );
357 }
358 return $stylesheet;
359 }
360
361 /**
362 * Generates CSS declarations for a block.
363 *
364 * @param string $block_selector CSS selector for the block.
365 * @param array $block_supports A list of properties supported by the block.
366 * @param array $block_styles The list of properties/values to be converted to CSS.
367 *
368 * @return string The corresponding CSS rule.
369 */
370 function gutenberg_experimental_global_styles_resolver_styles( $block_selector, $block_supports, $block_styles ) {
371 $css_rule = '';
372 $css_declarations = '';
373 foreach ( $block_styles as $property => $value ) {
374 // Only convert to CSS:
375 //
376 // 1) The style attributes the block has declared support for.
377 // 2) Any CSS custom property attached to the node.
378 if ( in_array( $property, $block_supports, true ) || strstr( $property, '--' ) ) {
379 $css_declarations .= "\t" . $property . ': ' . $value . ";\n";
380 }
381 }
382 if ( '' !== $css_declarations ) {
383 $css_rule .= $block_selector . " {\n";
384 $css_rule .= $css_declarations;
385 $css_rule .= "}\n";
386 }
387
388 return $css_rule;
389 }
390
391 /**
392 * Helper function that merges trees that adhere to the theme.json schema.
393 *
394 * @param array $core Core origin.
395 * @param array $theme Theme origin.
396 * @param array $user User origin. An empty array by default.
397 *
398 * @return array The merged result.
399 */
400 function gutenberg_experimental_global_styles_merge_trees( $core, $theme, $user = array() ) {
401 $core = gutenberg_experimental_global_styles_normalize_schema( $core );
402 $theme = gutenberg_experimental_global_styles_normalize_schema( $theme );
403 $user = gutenberg_experimental_global_styles_normalize_schema( $user );
404 $result = gutenberg_experimental_global_styles_normalize_schema( array() );
405
406 foreach ( array_keys( $core ) as $block_name ) {
407 foreach ( array( 'presets', 'styles', 'features' ) as $subtree ) {
408 $result[ $block_name ][ $subtree ] = array_merge(
409 $core[ $block_name ][ $subtree ],
410 $theme[ $block_name ][ $subtree ],
411 $user[ $block_name ][ $subtree ]
412 );
413 }
414 }
415
416 return $result;
417 }
418
419 /**
420 * Given a tree, it normalizes it to the expected schema.
421 *
422 * @param array $tree Source tree to normalize.
423 *
424 * @return array Normalized tree.
425 */
426 function gutenberg_experimental_global_styles_normalize_schema( $tree ) {
427 $block_schema = array(
428 'styles' => array(),
429 'features' => array(),
430 'presets' => array(),
431 );
432
433 $normalized_tree = array();
434 $block_data = gutenberg_experimental_global_styles_get_block_data();
435 foreach ( array_keys( $block_data ) as $block_name ) {
436 $normalized_tree[ $block_name ] = $block_schema;
437 }
438
439 $tree = array_merge_recursive(
440 $normalized_tree,
441 $tree
442 );
443
444 return $tree;
445 }
446
447 /**
448 * Returns the stylesheet resulting of merging
449 * core's, theme's, and user's origins.
450 *
451 * @return string
452 */
453 function gutenberg_experimental_global_styles_get_stylesheet() {
454 $gs_merged = array();
455 $gs_core = gutenberg_experimental_global_styles_get_core();
456 $gs_theme = gutenberg_experimental_global_styles_get_theme();
457 $gs_user = gutenberg_experimental_global_styles_get_user();
458
459 $gs_merged = gutenberg_experimental_global_styles_merge_trees( $gs_core, $gs_theme, $gs_user );
460
461 $stylesheet = gutenberg_experimental_global_styles_resolver( $gs_merged );
462 if ( empty( $stylesheet ) ) {
463 return;
464 }
465 return $stylesheet;
466 }
467
468 /**
469 * Fetches the preferences for each origin (core, theme, user)
470 * and enqueues the resulting stylesheet.
471 */
472 function gutenberg_experimental_global_styles_enqueue_assets() {
473 if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) {
474 return;
475 }
476
477 $stylesheet = gutenberg_experimental_global_styles_get_stylesheet();
478
479 wp_register_style( 'global-styles', false, array(), true, true );
480 wp_add_inline_style( 'global-styles', $stylesheet );
481 wp_enqueue_style( 'global-styles' );
482 }
483
484 /**
485 * Adds the necessary data for the Global Styles client UI to the block settings.
486 *
487 * @param array $settings Existing block editor settings.
488 * @return array New block editor settings
489 */
490 function gutenberg_experimental_global_styles_settings( $settings ) {
491 if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) {
492 return $settings;
493 }
494
495 $settings['__experimentalGlobalStylesUserEntityId'] = gutenberg_experimental_global_styles_get_user_cpt_id();
496
497 $global_styles = gutenberg_experimental_global_styles_merge_trees(
498 gutenberg_experimental_global_styles_get_core(),
499 gutenberg_experimental_global_styles_get_theme()
500 );
501
502 $settings['__experimentalGlobalStylesBase'] = $global_styles;
503
504 // Add the styles for the editor via the settings
505 // so they get processed as if they were added via add_editor_styles:
506 // they will get the editor wrapper class.
507 $settings['styles'][] = array( 'css' => gutenberg_experimental_global_styles_get_stylesheet() );
508
509 return $settings;
510 }
511
512 /**
513 * Registers a Custom Post Type to store the user's origin config.
514 */
515 function gutenberg_experimental_global_styles_register_cpt() {
516 if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) {
517 return;
518 }
519
520 $args = array(
521 'label' => __( 'Global Styles', 'gutenberg' ),
522 'description' => 'CPT to store user design tokens',
523 'public' => false,
524 'show_ui' => false,
525 'show_in_rest' => true,
526 'rest_base' => '__experimental/global-styles',
527 'capabilities' => array(
528 'read' => 'edit_theme_options',
529 'create_posts' => 'edit_theme_options',
530 'edit_posts' => 'edit_theme_options',
531 'edit_published_posts' => 'edit_theme_options',
532 'delete_published_posts' => 'edit_theme_options',
533 'edit_others_posts' => 'edit_theme_options',
534 'delete_others_posts' => 'edit_theme_options',
535 ),
536 'map_meta_cap' => true,
537 'supports' => array(
538 'editor',
539 'revisions',
540 ),
541 );
542 register_post_type( 'wp_global_styles', $args );
543 }
544
545 add_action( 'init', 'gutenberg_experimental_global_styles_register_cpt' );
546 add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings' );
547 add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' );
548