PluginProbe
Gutenberg / 9.7.3
Gutenberg v9.7.3
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.7.3, at lib/class-wp-theme-json.php

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