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

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