PluginProbe
Gutenberg / 9.5.1
Gutenberg v9.5.1
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 / class-wp-theme-json.php

class-wp-theme-json.php in Gutenberg 9.5.1, at lib/class-wp-theme-json.php

1,009 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Process of structures that adhere to the theme.json schema.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Class that encapsulates the processing of
10 * structures that adhere to the theme.json spec.
11 */
12 class WP_Theme_JSON {
13
14 /**
15 * Container of data in theme.json format.
16 *
17 * @var array
18 */
19 private $contexts = null;
20
21 /**
22 * Holds block metadata extracted from block.json
23 * to be shared among all instances so we don't
24 * process it twice.
25 *
26 * @var array
27 */
28 private static $blocks_metadata = null;
29
30 /**
31 * The name of the global context.
32 *
33 * @var string
34 */
35 const GLOBAL_NAME = 'global';
36
37 /**
38 * The CSS selector for the global context.
39 *
40 * @var string
41 */
42 const GLOBAL_SELECTOR = ':root';
43
44 /**
45 * The supported properties of the global context.
46 *
47 * @var array
48 */
49 const GLOBAL_SUPPORTS = array(
50 '--wp--style--color--link',
51 'background',
52 'backgroundColor',
53 'color',
54 'fontFamily',
55 'fontSize',
56 'fontStyle',
57 'fontWeight',
58 'lineHeight',
59 'textDecoration',
60 'textTransform',
61 );
62
63 /**
64 * Data schema of each context within a theme.json.
65 *
66 * Example:
67 *
68 * {
69 * 'context-one': {
70 * 'styles': {
71 * 'color': {
72 * 'background': 'color'
73 * }
74 * },
75 * 'settings': {
76 * 'color': {
77 * 'custom': true
78 * }
79 * }
80 * },
81 * 'context-two': {
82 * 'styles': {
83 * 'color': {
84 * 'link': 'color'
85 * }
86 * }
87 * }
88 * }
89 */
90 const SCHEMA = array(
91 'selector' => null,
92 'supports' => null,
93 'styles' => array(
94 'color' => array(
95 'background' => null,
96 'gradient' => null,
97 'link' => null,
98 'text' => null,
99 ),
100 'spacing' => array(
101 'padding' => array(
102 'top' => null,
103 'right' => null,
104 'bottom' => null,
105 'left' => null,
106 ),
107 ),
108 'typography' => array(
109 'fontFamily' => null,
110 'fontSize' => null,
111 'fontStyle' => null,
112 'fontWeight' => null,
113 'lineHeight' => null,
114 'textDecoration' => null,
115 'textTransform' => null,
116 ),
117 ),
118 'settings' => array(
119 'color' => array(
120 'custom' => null,
121 'customGradient' => null,
122 'gradients' => null,
123 'link' => null,
124 'palette' => null,
125 ),
126 'spacing' => array(
127 'customPadding' => null,
128 'units' => null,
129 ),
130 'typography' => array(
131 'customFontSize' => null,
132 'customLineHeight' => null,
133 'dropCap' => null,
134 'fontFamilies' => null,
135 'fontSizes' => null,
136 'fontStyles' => null,
137 'fontWeights' => null,
138 'textDecorations' => null,
139 'textTransforms' => null,
140 ),
141 'custom' => null,
142 ),
143 );
144
145 /**
146 * Presets are a set of values that serve
147 * to bootstrap some styles: colors, font sizes, etc.
148 *
149 * They are a unkeyed array of values such as:
150 *
151 * ```php
152 * array(
153 * array(
154 * 'slug' => 'unique-name-within-the-set',
155 * 'name' => 'Name for the UI',
156 * <value_key> => 'value'
157 * ),
158 * )
159 * ```
160 *
161 * This contains the necessary metadata to process them:
162 *
163 * - path => where to find the preset in a theme.json context
164 *
165 * - value_key => the key that represents the value
166 *
167 * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
168 * --wp--preset--<preset_infix>--<slug>: <preset_value>
169 *
170 * - classes => array containing a structure with the classes to
171 * generate for the presets. Each class should have
172 * the class suffix and the property name. Example:
173 *
174 * .has-<slug>-<class_suffix> {
175 * <property_name>: <preset_value>
176 * }
177 */
178 const PRESETS_METADATA = array(
179 array(
180 'path' => array( 'settings', 'color', 'palette' ),
181 'value_key' => 'color',
182 'css_var_infix' => 'color',
183 'classes' => array(
184 array(
185 'class_suffix' => 'color',
186 'property_name' => 'color',
187 ),
188 array(
189 'class_suffix' => 'background-color',
190 'property_name' => 'background-color',
191 ),
192 ),
193 ),
194 array(
195 'path' => array( 'settings', 'color', 'gradients' ),
196 'value_key' => 'gradient',
197 'css_var_infix' => 'gradient',
198 'classes' => array(
199 array(
200 'class_suffix' => 'gradient-background',
201 'property_name' => 'background',
202 ),
203 ),
204 ),
205 array(
206 'path' => array( 'settings', 'typography', 'fontSizes' ),
207 'value_key' => 'size',
208 'css_var_infix' => 'font-size',
209 'classes' => array(
210 array(
211 'class_suffix' => 'font-size',
212 'property_name' => 'font-size',
213 ),
214 ),
215 ),
216 array(
217 'path' => array( 'settings', 'typography', 'fontFamilies' ),
218 'value_key' => 'fontFamily',
219 'css_var_infix' => 'font-family',
220 'classes' => array(),
221 ),
222 array(
223 'path' => array( 'settings', 'typography', 'fontStyles' ),
224 'value_key' => 'value',
225 'css_var_infix' => 'font-style',
226 'classes' => array(
227 array(
228 'class_suffix' => 'font-style',
229 'property_name' => 'font-style',
230 ),
231 ),
232 ),
233 array(
234 'path' => array( 'settings', 'typography', 'fontWeights' ),
235 'value_key' => 'value',
236 'css_var_infix' => 'font-weight',
237 'classes' => array(
238 array(
239 'class_suffix' => 'font-weight',
240 'property_name' => 'font-weight',
241 ),
242 ),
243 ),
244 array(
245 'path' => array( 'settings', 'typography', 'textDecorations' ),
246 'value_key' => 'value',
247 'css_var_infix' => 'text-decoration',
248 'classes' => array(
249 array(
250 'class_suffix' => 'text-decoration',
251 'property_name' => 'text-decoration',
252 ),
253 ),
254 ),
255 array(
256 'path' => array( 'settings', 'typography', 'textTransforms' ),
257 'value_key' => 'slug',
258 'css_var_infix' => 'text-transform',
259 'classes' => array(
260 array(
261 'class_suffix' => 'text-transform',
262 'property_name' => 'text-transform',
263 ),
264 ),
265 ),
266 );
267
268 /**
269 * Metadata for style properties.
270 *
271 * Each property declares:
272 *
273 * - 'value': path to the value in theme.json and block attributes.
274 * - 'support': path to the block support in block.json.
275 */
276 const PROPERTIES_METADATA = array(
277 '--wp--style--color--link' => array(
278 'value' => array( 'color', 'link' ),
279 'support' => array( 'color', 'link' ),
280 ),
281 'background' => array(
282 'value' => array( 'color', 'gradient' ),
283 'support' => array( 'color', 'gradients' ),
284 ),
285 'backgroundColor' => array(
286 'value' => array( 'color', 'background' ),
287 'support' => array( 'color' ),
288 ),
289 'color' => array(
290 'value' => array( 'color', 'text' ),
291 'support' => array( 'color' ),
292 ),
293 'fontFamily' => array(
294 'value' => array( 'typography', 'fontFamily' ),
295 'support' => array( '__experimentalFontFamily' ),
296 ),
297 'fontSize' => array(
298 'value' => array( 'typography', 'fontSize' ),
299 'support' => array( 'fontSize' ),
300 ),
301 'fontStyle' => array(
302 'value' => array( 'typography', 'fontStyle' ),
303 'support' => array( '__experimentalFontStyle' ),
304 ),
305 'fontWeight' => array(
306 'value' => array( 'typography', 'fontWeight' ),
307 'support' => array( '__experimentalFontWeight' ),
308 ),
309 'lineHeight' => array(
310 'value' => array( 'typography', 'lineHeight' ),
311 'support' => array( 'lineHeight' ),
312 ),
313 'paddingBottom' => array(
314 'value' => array( 'spacing', 'padding', 'bottom' ),
315 'support' => array( 'spacing', 'padding' ),
316 ),
317 'paddingLeft' => array(
318 'value' => array( 'spacing', 'padding', 'left' ),
319 'support' => array( 'spacing', 'padding' ),
320 ),
321 'paddingRight' => array(
322 'value' => array( 'spacing', 'padding', 'right' ),
323 'support' => array( 'spacing', 'padding' ),
324 ),
325 'paddingTop' => array(
326 'value' => array( 'spacing', 'padding', 'top' ),
327 'support' => array( 'spacing', 'padding' ),
328 ),
329 'textDecoration' => array(
330 'value' => array( 'typography', 'textDecoration' ),
331 'support' => array( '__experimentalTextDecoration' ),
332 ),
333 'textTransform' => array(
334 'value' => array( 'typography', 'textTransform' ),
335 'support' => array( '__experimentalTextTransform' ),
336 ),
337 );
338
339 /**
340 * Constructor.
341 *
342 * @param array $contexts A structure that follows the theme.json schema.
343 */
344 public function __construct( $contexts = array() ) {
345 $this->contexts = array();
346
347 if ( ! is_array( $contexts ) ) {
348 return;
349 }
350
351 $metadata = $this->get_blocks_metadata();
352 foreach ( $contexts as $key => $context ) {
353 if ( ! isset( $metadata[ $key ] ) ) {
354 // Skip incoming contexts that can't be found
355 // within the contexts registered.
356 continue;
357 }
358
359 // Filter out top-level keys that aren't valid according to the schema.
360 $context = array_intersect_key( $context, self::SCHEMA );
361
362 // Selector & Supports are always taken from metadata.
363 $this->contexts[ $key ]['selector'] = $metadata[ $key ]['selector'];
364 $this->contexts[ $key ]['supports'] = $metadata[ $key ]['supports'];
365
366 // Process styles subtree.
367 $this->process_key( 'styles', $context, self::SCHEMA );
368 if ( isset( $context['styles'] ) ) {
369 $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'] );
370 $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'] );
371
372 if ( empty( $context['styles'] ) ) {
373 unset( $context['styles'] );
374 } else {
375 $this->contexts[ $key ]['styles'] = $context['styles'];
376 }
377 }
378
379 // Process settings subtree.
380 $this->process_key( 'settings', $context, self::SCHEMA );
381 if ( isset( $context['settings'] ) ) {
382 $this->process_key( 'color', $context['settings'], self::SCHEMA['settings'] );
383 $this->process_key( 'spacing', $context['settings'], self::SCHEMA['settings'] );
384 $this->process_key( 'typography', $context['settings'], self::SCHEMA['settings'] );
385
386 if ( empty( $context['settings'] ) ) {
387 unset( $context['settings'] );
388 } else {
389 $this->contexts[ $key ]['settings'] = $context['settings'];
390 }
391 }
392 }
393 }
394
395 /**
396 * Returns the metadata for each block.
397 *
398 * Example:
399 *
400 * {
401 * 'global': {
402 * 'selector': ':root'
403 * 'supports': [ 'fontSize', 'backgroundColor' ],
404 * },
405 * 'core/heading/h1': {
406 * 'selector': 'h1'
407 * 'supports': [ 'fontSize', 'backgroundColor' ],
408 * }
409 * }
410 *
411 * @return array Block metadata.
412 */
413 private static function get_blocks_metadata() {
414 if ( null !== self::$blocks_metadata ) {
415 return self::$blocks_metadata;
416 }
417
418 self::$blocks_metadata = array(
419 self::GLOBAL_NAME => array(
420 'selector' => self::GLOBAL_SELECTOR,
421 'supports' => self::GLOBAL_SUPPORTS,
422 ),
423 );
424
425 $registry = WP_Block_Type_Registry::get_instance();
426 $blocks = $registry->get_all_registered();
427 foreach ( $blocks as $block_name => $block_type ) {
428 /*
429 * Skips blocks that don't declare support,
430 * they don't generate styles.
431 */
432 if (
433 ! property_exists( $block_type, 'supports' ) ||
434 ! is_array( $block_type->supports ) ||
435 empty( $block_type->supports )
436 ) {
437 continue;
438 }
439
440 /*
441 * Extract block support keys that are related to the style properties.
442 */
443 $block_supports = array();
444 foreach ( self::PROPERTIES_METADATA as $key => $metadata ) {
445 if ( gutenberg_experimental_get( $block_type->supports, $metadata['support'] ) ) {
446 $block_supports[] = $key;
447 }
448 }
449
450 /*
451 * Skip blocks that don't support anything related to styles.
452 */
453 if ( empty( $block_supports ) ) {
454 continue;
455 }
456
457 /*
458 * Assign the selector for the block.
459 *
460 * Some blocks can declare multiple selectors:
461 *
462 * - core/heading represents the H1-H6 HTML elements
463 * - core/list represents the UL and OL HTML elements
464 * - core/group is meant to represent DIV and other HTML elements
465 *
466 * Some other blocks don't provide a selector,
467 * so we generate a class for them based on their name:
468 *
469 * - 'core/group' => '.wp-block-group'
470 * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name'
471 *
472 * Note that, for core blocks, we don't add the `core/` prefix to its class name.
473 * This is for historical reasons, as they come with a class without that infix.
474 *
475 */
476 if (
477 isset( $block_type->supports['__experimentalSelector'] ) &&
478 is_string( $block_type->supports['__experimentalSelector'] )
479 ) {
480 self::$blocks_metadata[ $block_name ] = array(
481 'selector' => $block_type->supports['__experimentalSelector'],
482 'supports' => $block_supports,
483 );
484 } elseif (
485 isset( $block_type->supports['__experimentalSelector'] ) &&
486 is_array( $block_type->supports['__experimentalSelector'] )
487 ) {
488 foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector_metadata ) {
489 if ( ! isset( $selector_metadata['selector'] ) ) {
490 continue;
491 }
492
493 self::$blocks_metadata[ $key ] = array(
494 'selector' => $selector_metadata['selector'],
495 'supports' => $block_supports,
496 );
497 }
498 } else {
499 self::$blocks_metadata[ $block_name ] = array(
500 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ),
501 'supports' => $block_supports,
502 );
503 }
504 }
505
506 return self::$blocks_metadata;
507 }
508
509 /**
510 * Normalize the subtree according to the given schema.
511 * This function modifies the given input by removing
512 * the nodes that aren't valid per the schema.
513 *
514 * @param string $key Key of the subtree to normalize.
515 * @param array $input Whole tree to normalize.
516 * @param array $schema Schema to use for normalization.
517 */
518 private static function process_key( $key, &$input, $schema ) {
519 if ( ! isset( $input[ $key ] ) ) {
520 return;
521 }
522
523 // Consider valid the input value.
524 if ( null === $schema[ $key ] ) {
525 return;
526 }
527
528 if ( ! is_array( $input[ $key ] ) ) {
529 unset( $input[ $key ] );
530 return;
531 }
532
533 $input[ $key ] = array_intersect_key(
534 $input[ $key ],
535 $schema[ $key ]
536 );
537
538 if ( 0 === count( $input[ $key ] ) ) {
539 unset( $input[ $key ] );
540 }
541 }
542
543 /**
544 * Given a context, it returns its settings subtree.
545 *
546 * @param array $context Context adhering to the theme.json schema.
547 *
548 * @return array|null The settings subtree.
549 */
550 private static function extract_settings( $context ) {
551 if ( empty( $context['settings'] ) ) {
552 return null;
553 }
554
555 return $context['settings'];
556 }
557
558 /**
559 * Given a tree, it creates a flattened one
560 * by merging the keys and binding the leaf values
561 * to the new keys.
562 *
563 * It also transforms camelCase names into kebab-case
564 * and substitutes '/' by '-'.
565 *
566 * This is thought to be useful to generate
567 * CSS Custom Properties from a tree,
568 * although there's nothing in the implementation
569 * of this function that requires that format.
570 *
571 * For example, assuming the given prefix is '--wp'
572 * and the token is '--', for this input tree:
573 *
574 * {
575 * 'some/property': 'value',
576 * 'nestedProperty': {
577 * 'sub-property': 'value'
578 * }
579 * }
580 *
581 * it'll return this output:
582 *
583 * {
584 * '--wp--some-property': 'value',
585 * '--wp--nested-property--sub-property': 'value'
586 * }
587 *
588 * @param array $tree Input tree to process.
589 * @param string $prefix Prefix to prepend to each variable. '' by default.
590 * @param string $token Token to use between levels. '--' by default.
591 *
592 * @return array The flattened tree.
593 */
594 private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
595 $result = array();
596 foreach ( $tree as $property => $value ) {
597 $new_key = $prefix . str_replace(
598 '/',
599 '-',
600 strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
601 );
602
603 if ( is_array( $value ) ) {
604 $new_prefix = $new_key . $token;
605 $result = array_merge(
606 $result,
607 self::flatten_tree( $value, $new_prefix, $token )
608 );
609 } else {
610 $result[ $new_key ] = $value;
611 }
612 }
613 return $result;
614 }
615
616 /**
617 * Returns the style property for the given path.
618 *
619 * It also converts CSS Custom Property stored as
620 * "var:preset|color|secondary" to the form
621 * "--wp--preset--color--secondary".
622 *
623 * @param array $styles Styles subtree.
624 * @param array $path Which property to process.
625 *
626 * @return string Style property value.
627 */
628 private static function get_property_value( $styles, $path ) {
629 $value = gutenberg_experimental_get( $styles, $path, '' );
630
631 if ( '' === $value ) {
632 return $value;
633 }
634
635 $prefix = 'var:';
636 $prefix_len = strlen( $prefix );
637 $token_in = '|';
638 $token_out = '--';
639 if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
640 $unwrapped_name = str_replace(
641 $token_in,
642 $token_out,
643 substr( $value, $prefix_len )
644 );
645 $value = "var(--wp--$unwrapped_name)";
646 }
647
648 return $value;
649 }
650
651 /**
652 * Given a context, it extracts the style properties
653 * and adds them to the $declarations array following the format:
654 *
655 * ```php
656 * array(
657 * 'name' => 'property_name',
658 * 'value' => 'property_value,
659 * )
660 * ```
661 *
662 * Note that this modifies the $declarations in place.
663 *
664 * @param array $declarations Holds the existing declarations.
665 * @param array $context Input context to process.
666 */
667 private static function compute_style_properties( &$declarations, $context ) {
668 if ( empty( $context['supports'] ) || empty( $context['styles'] ) ) {
669 return;
670 }
671
672 foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
673 if ( ! in_array( $name, $context['supports'], true ) ) {
674 continue;
675 }
676
677 $value = self::get_property_value( $context['styles'], $metadata['value'] );
678 if ( ! empty( $value ) ) {
679 $kebabcased_name = strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $name ) );
680 $declarations[] = array(
681 'name' => $kebabcased_name,
682 'value' => $value,
683 );
684 }
685 }
686 }
687
688 /**
689 * Given a context, it extracts its presets
690 * and adds them to the given input $stylesheet.
691 *
692 * Note this function modifies $stylesheet in place.
693 *
694 * @param string $stylesheet Input stylesheet to add the presets to.
695 * @param array $context Context to process.
696 */
697 private static function compute_preset_classes( &$stylesheet, $context ) {
698 $selector = $context['selector'];
699 if ( self::GLOBAL_SELECTOR === $selector ) {
700 // Classes at the global level do not need any CSS prefixed,
701 // and we don't want to increase its specificity.
702 $selector = '';
703 }
704
705 foreach ( self::PRESETS_METADATA as $preset ) {
706 $values = gutenberg_experimental_get( $context, $preset['path'], array() );
707 foreach ( $values as $value ) {
708 foreach ( $preset['classes'] as $class ) {
709 $stylesheet .= self::to_ruleset(
710 $selector . '.has-' . $value['slug'] . '-' . $class['class_suffix'],
711 array(
712 array(
713 'name' => $class['property_name'],
714 'value' => $value[ $preset['value_key'] ],
715 ),
716 )
717 );
718 }
719 }
720 }
721 }
722
723 /**
724 * Given a context, it extracts the CSS Custom Properties
725 * for the presets and adds them to the $declarations array
726 * following the format:
727 *
728 * ```php
729 * array(
730 * 'name' => 'property_name',
731 * 'value' => 'property_value,
732 * )
733 * ```
734 *
735 * Note that this modifies the $declarations in place.
736 *
737 * @param array $declarations Holds the existing declarations.
738 * @param array $context Input context to process.
739 */
740 private static function compute_preset_vars( &$declarations, $context ) {
741 foreach ( self::PRESETS_METADATA as $preset ) {
742 $values = gutenberg_experimental_get( $context, $preset['path'], array() );
743 foreach ( $values as $value ) {
744 $declarations[] = array(
745 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'],
746 'value' => $value[ $preset['value_key'] ],
747 );
748 }
749 }
750 }
751
752 /**
753 * Given a context, it extracts the CSS Custom Properties
754 * for the custom values and adds them to the $declarations
755 * array following the format:
756 *
757 * ```php
758 * array(
759 * 'name' => 'property_name',
760 * 'value' => 'property_value,
761 * )
762 * ```
763 *
764 * Note that this modifies the $declarations in place.
765 *
766 * @param array $declarations Holds the existing declarations.
767 * @param array $context Input context to process.
768 */
769 private static function compute_theme_vars( &$declarations, $context ) {
770 $custom_values = gutenberg_experimental_get( $context, array( 'settings', 'custom' ) );
771 $css_vars = self::flatten_tree( $custom_values );
772 foreach ( $css_vars as $key => $value ) {
773 $declarations[] = array(
774 'name' => '--wp--custom--' . $key,
775 'value' => $value,
776 );
777 }
778 }
779
780 /**
781 * Given a selector and a declaration list,
782 * creates the corresponding ruleset.
783 *
784 * To help debugging, will add some space
785 * if SCRIPT_DEBUG is defined and true.
786 *
787 * @param string $selector CSS selector.
788 * @param array $declarations List of declarations.
789 *
790 * @return string CSS ruleset.
791 */
792 private static function to_ruleset( $selector, $declarations ) {
793 if ( empty( $declarations ) ) {
794 return '';
795 }
796 $ruleset = '';
797
798 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
799 $declaration_block = array_reduce(
800 $declarations,
801 function ( $carry, $element ) {
802 return $carry .= "\t" . $element['name'] . ': ' . $element['value'] . ";\n"; },
803 ''
804 );
805 $ruleset .= $selector . " {\n" . $declaration_block . "}\n";
806 } else {
807 $declaration_block = array_reduce(
808 $declarations,
809 function ( $carry, $element ) {
810 return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
811 ''
812 );
813 $ruleset .= $selector . '{' . $declaration_block . '}';
814 }
815
816 return $ruleset;
817 }
818
819 /**
820 * Converts each context into a list of rulesets
821 * to be appended to the stylesheet.
822 * These rulesets contain all the css variables (custom variables and preset variables).
823 *
824 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
825 *
826 * For each context this creates a new ruleset such as:
827 *
828 * context-selector {
829 * --wp--preset--category--slug: value;
830 * --wp--custom--variable: value;
831 * }
832 *
833 * @param string $stylesheet Stylesheet to append new rules to.
834 * @param array $context Context to be processed.
835 *
836 * @return string The new stylesheet.
837 */
838 private static function to_css_variables( $stylesheet, $context ) {
839 if ( empty( $context['selector'] ) ) {
840 return $stylesheet;
841 }
842
843 $declarations = array();
844 self::compute_preset_vars( $declarations, $context );
845 self::compute_theme_vars( $declarations, $context );
846
847 // Attach the ruleset for style and custom properties.
848 $stylesheet .= self::to_ruleset( $context['selector'], $declarations );
849
850 return $stylesheet;
851 }
852
853 /**
854 * Converts each context into a list of rulesets
855 * containing the block styles to be appended to the stylesheet.
856 *
857 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
858 *
859 * For each context this creates a new ruleset such as:
860 *
861 * context-selector {
862 * style-property-one: value;
863 * }
864 *
865 * Additionally, it'll also create new rulesets
866 * as classes for each preset value such as:
867 *
868 * .has-value-color {
869 * color: value;
870 * }
871 *
872 * .has-value-background-color {
873 * background-color: value;
874 * }
875 *
876 * .has-value-font-size {
877 * font-size: value;
878 * }
879 *
880 * .has-value-gradient-background {
881 * background: value;
882 * }
883 *
884 * p.has-value-gradient-background {
885 * background: value;
886 * }
887 *
888 * @param string $stylesheet Stylesheet to append new rules to.
889 * @param array $context Context to be processed.
890 *
891 * @return string The new stylesheet.
892 */
893 private static function to_block_styles( $stylesheet, $context ) {
894 if ( empty( $context['selector'] ) ) {
895 return $stylesheet;
896 }
897
898 $declarations = array();
899 self::compute_style_properties( $declarations, $context );
900
901 $stylesheet .= self::to_ruleset( $context['selector'], $declarations );
902
903 // Attach the rulesets for the classes.
904 self::compute_preset_classes( $stylesheet, $context );
905
906 return $stylesheet;
907 }
908
909 /**
910 * Returns the existing settings for each context.
911 *
912 * Example:
913 *
914 * {
915 * 'global': {
916 * 'color': {
917 * 'custom': true
918 * }
919 * },
920 * 'core/paragraph': {
921 * 'spacing': {
922 * 'customPadding': true
923 * }
924 * }
925 * }
926 *
927 * @return array Settings per context.
928 */
929 public function get_settings() {
930 return array_filter(
931 array_map( array( $this, 'extract_settings' ), $this->contexts ),
932 function ( $element ) {
933 return null !== $element;
934 }
935 );
936 }
937
938 /**
939 * Returns the stylesheet that results of processing
940 * the theme.json structure this object represents.
941 *
942 * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'.
943 * @return string Stylesheet.
944 */
945 public function get_stylesheet( $type = 'all' ) {
946 switch ( $type ) {
947 case 'block_styles':
948 return array_reduce( $this->contexts, array( $this, 'to_block_styles' ), '' );
949 case 'css_variables':
950 return array_reduce( $this->contexts, array( $this, 'to_css_variables' ), '' );
951 default:
952 return array_reduce( $this->contexts, array( $this, 'to_css_variables' ), '' ) . array_reduce( $this->contexts, array( $this, 'to_block_styles' ), '' );
953 }
954 }
955
956 /**
957 * Merge new incoming data.
958 *
959 * @param WP_Theme_JSON $theme_json Data to merge.
960 */
961 public function merge( $theme_json ) {
962 $incoming_data = $theme_json->get_raw_data();
963 $metadata = $this->get_blocks_metadata();
964
965 foreach ( array_keys( $incoming_data ) as $context ) {
966 // Selector & Supports are always taken from metadata.
967 $this->contexts[ $context ]['selector'] = $metadata[ $context ]['selector'];
968 $this->contexts[ $context ]['supports'] = $metadata[ $context ]['supports'];
969
970 foreach ( array( 'settings', 'styles' ) as $subtree ) {
971 if ( ! isset( $incoming_data[ $context ][ $subtree ] ) ) {
972 continue;
973 }
974
975 if ( ! isset( $this->contexts[ $context ][ $subtree ] ) ) {
976 $this->contexts[ $context ][ $subtree ] = $incoming_data[ $context ][ $subtree ];
977 continue;
978 }
979
980 foreach ( array_keys( self::SCHEMA[ $subtree ] ) as $leaf ) {
981 if ( ! isset( $incoming_data[ $context ][ $subtree ][ $leaf ] ) ) {
982 continue;
983 }
984
985 if ( ! isset( $this->contexts[ $context ][ $subtree ][ $leaf ] ) ) {
986 $this->contexts[ $context ][ $subtree ][ $leaf ] = $incoming_data[ $context ][ $subtree ][ $leaf ];
987 continue;
988 }
989
990 $this->contexts[ $context ][ $subtree ][ $leaf ] = array_merge(
991 $this->contexts[ $context ][ $subtree ][ $leaf ],
992 $incoming_data[ $context ][ $subtree ][ $leaf ]
993 );
994 }
995 }
996 }
997 }
998
999 /**
1000 * Retuns the raw data.
1001 *
1002 * @return array Raw data.
1003 */
1004 public function get_raw_data() {
1005 return $this->contexts;
1006 }
1007
1008 }
1009