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

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

986 lines 26.1 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 'paddingBottom' => array(
276 'value' => array( 'spacing', 'padding', 'bottom' ),
277 'support' => array( 'spacing', 'padding' ),
278 ),
279 'paddingLeft' => array(
280 'value' => array( 'spacing', 'padding', 'left' ),
281 'support' => array( 'spacing', 'padding' ),
282 ),
283 'paddingRight' => array(
284 'value' => array( 'spacing', 'padding', 'right' ),
285 'support' => array( 'spacing', 'padding' ),
286 ),
287 'paddingTop' => array(
288 'value' => array( 'spacing', 'padding', 'top' ),
289 'support' => array( 'spacing', 'padding' ),
290 ),
291 'textDecoration' => array(
292 'value' => array( 'typography', 'textDecoration' ),
293 'support' => array( '__experimentalTextDecoration' ),
294 ),
295 'textTransform' => array(
296 'value' => array( 'typography', 'textTransform' ),
297 'support' => array( '__experimentalTextTransform' ),
298 ),
299 );
300
301 /**
302 * Constructor.
303 *
304 * @param array $contexts A structure that follows the theme.json schema.
305 * @param boolean $should_escape_styles Whether the incoming styles should be escaped.
306 */
307 public function __construct( $contexts = array(), $should_escape_styles = false ) {
308 $this->contexts = array();
309
310 if ( ! is_array( $contexts ) ) {
311 return;
312 }
313
314 $metadata = $this->get_blocks_metadata();
315 foreach ( $contexts as $key => $context ) {
316 if ( ! isset( $metadata[ $key ] ) ) {
317 // Skip incoming contexts that can't be found
318 // within the contexts registered.
319 continue;
320 }
321
322 // Filter out top-level keys that aren't valid according to the schema.
323 $context = array_intersect_key( $context, self::SCHEMA );
324
325 // Process styles subtree.
326 $this->process_key( 'styles', $context, self::SCHEMA );
327 if ( isset( $context['styles'] ) ) {
328 $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
329 $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
330 $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
331
332 if ( empty( $context['styles'] ) ) {
333 unset( $context['styles'] );
334 } else {
335 $this->contexts[ $key ]['styles'] = $context['styles'];
336 }
337 }
338
339 // Process settings subtree.
340 $this->process_key( 'settings', $context, self::SCHEMA );
341 if ( isset( $context['settings'] ) ) {
342 $this->process_key( 'border', $context['settings'], self::SCHEMA['settings'] );
343 $this->process_key( 'color', $context['settings'], self::SCHEMA['settings'] );
344 $this->process_key( 'spacing', $context['settings'], self::SCHEMA['settings'] );
345 $this->process_key( 'typography', $context['settings'], self::SCHEMA['settings'] );
346
347 if ( empty( $context['settings'] ) ) {
348 unset( $context['settings'] );
349 } else {
350 $this->contexts[ $key ]['settings'] = $context['settings'];
351 }
352 }
353 }
354 }
355
356 /**
357 * Returns the metadata for each block.
358 *
359 * Example:
360 *
361 * {
362 * 'global': {
363 * 'selector': ':root'
364 * 'supports': [ 'fontSize', 'backgroundColor' ],
365 * },
366 * 'core/heading/h1': {
367 * 'selector': 'h1'
368 * 'supports': [ 'fontSize', 'backgroundColor' ],
369 * }
370 * }
371 *
372 * @return array Block metadata.
373 */
374 private static function get_blocks_metadata() {
375 if ( null !== self::$blocks_metadata ) {
376 return self::$blocks_metadata;
377 }
378
379 self::$blocks_metadata = array(
380 self::GLOBAL_NAME => array(
381 'selector' => self::GLOBAL_SELECTOR,
382 'supports' => self::GLOBAL_SUPPORTS,
383 ),
384 );
385
386 $registry = WP_Block_Type_Registry::get_instance();
387 $blocks = $registry->get_all_registered();
388 foreach ( $blocks as $block_name => $block_type ) {
389 /*
390 * Skips blocks that don't declare support,
391 * they don't generate styles.
392 */
393 if (
394 ! property_exists( $block_type, 'supports' ) ||
395 ! is_array( $block_type->supports ) ||
396 empty( $block_type->supports )
397 ) {
398 continue;
399 }
400
401 /*
402 * Extract block support keys that are related to the style properties.
403 */
404 $block_supports = array();
405 foreach ( self::PROPERTIES_METADATA as $key => $metadata ) {
406 if ( gutenberg_experimental_get( $block_type->supports, $metadata['support'] ) ) {
407 $block_supports[] = $key;
408 }
409 }
410
411 /*
412 * Skip blocks that don't support anything related to styles.
413 */
414 if ( empty( $block_supports ) ) {
415 continue;
416 }
417
418 /*
419 * Assign the selector for the block.
420 *
421 * Some blocks can declare multiple selectors:
422 *
423 * - core/heading represents the H1-H6 HTML elements
424 * - core/list represents the UL and OL HTML elements
425 * - core/group is meant to represent DIV and other HTML elements
426 *
427 * Some other blocks don't provide a selector,
428 * so we generate a class for them based on their name:
429 *
430 * - 'core/group' => '.wp-block-group'
431 * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name'
432 *
433 * Note that, for core blocks, we don't add the `core/` prefix to its class name.
434 * This is for historical reasons, as they come with a class without that infix.
435 *
436 */
437 if (
438 isset( $block_type->supports['__experimentalSelector'] ) &&
439 is_string( $block_type->supports['__experimentalSelector'] )
440 ) {
441 self::$blocks_metadata[ $block_name ] = array(
442 'selector' => $block_type->supports['__experimentalSelector'],
443 'supports' => $block_supports,
444 );
445 } elseif (
446 isset( $block_type->supports['__experimentalSelector'] ) &&
447 is_array( $block_type->supports['__experimentalSelector'] )
448 ) {
449 foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector_metadata ) {
450 if ( ! isset( $selector_metadata['selector'] ) ) {
451 continue;
452 }
453
454 self::$blocks_metadata[ $key ] = array(
455 'selector' => $selector_metadata['selector'],
456 'supports' => $block_supports,
457 );
458 }
459 } else {
460 self::$blocks_metadata[ $block_name ] = array(
461 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ),
462 'supports' => $block_supports,
463 );
464 }
465 }
466
467 return self::$blocks_metadata;
468 }
469
470 /**
471 * Normalize the subtree according to the given schema.
472 * This function modifies the given input by removing
473 * the nodes that aren't valid per the schema.
474 *
475 * @param string $key Key of the subtree to normalize.
476 * @param array $input Whole tree to normalize.
477 * @param array $schema Schema to use for normalization.
478 * @param boolean $should_escape Whether the subproperties should be escaped.
479 */
480 private static function process_key( $key, &$input, $schema, $should_escape = false ) {
481 if ( ! isset( $input[ $key ] ) ) {
482 return;
483 }
484
485 // Consider valid the input value.
486 if ( null === $schema[ $key ] ) {
487 return;
488 }
489
490 if ( ! is_array( $input[ $key ] ) ) {
491 unset( $input[ $key ] );
492 return;
493 }
494
495 $input[ $key ] = array_intersect_key(
496 $input[ $key ],
497 $schema[ $key ]
498 );
499
500 if ( $should_escape ) {
501 $subtree = $input[ $key ];
502 foreach ( $subtree as $property => $value ) {
503 $name = 'background-color';
504 if ( 'gradient' === $property ) {
505 $name = 'background';
506 }
507 $result = safecss_filter_attr( "$name: $value" );
508
509 if ( '' === $result ) {
510 unset( $input[ $key ][ $property ] );
511 }
512 }
513 }
514
515 if ( 0 === count( $input[ $key ] ) ) {
516 unset( $input[ $key ] );
517 }
518 }
519
520 /**
521 * Given a context, it returns its settings subtree.
522 *
523 * @param array $context Context adhering to the theme.json schema.
524 *
525 * @return array|null The settings subtree.
526 */
527 private static function extract_settings( $context ) {
528 if ( empty( $context['settings'] ) ) {
529 return null;
530 }
531
532 return $context['settings'];
533 }
534
535 /**
536 * Given a tree, it creates a flattened one
537 * by merging the keys and binding the leaf values
538 * to the new keys.
539 *
540 * It also transforms camelCase names into kebab-case
541 * and substitutes '/' by '-'.
542 *
543 * This is thought to be useful to generate
544 * CSS Custom Properties from a tree,
545 * although there's nothing in the implementation
546 * of this function that requires that format.
547 *
548 * For example, assuming the given prefix is '--wp'
549 * and the token is '--', for this input tree:
550 *
551 * {
552 * 'some/property': 'value',
553 * 'nestedProperty': {
554 * 'sub-property': 'value'
555 * }
556 * }
557 *
558 * it'll return this output:
559 *
560 * {
561 * '--wp--some-property': 'value',
562 * '--wp--nested-property--sub-property': 'value'
563 * }
564 *
565 * @param array $tree Input tree to process.
566 * @param string $prefix Prefix to prepend to each variable. '' by default.
567 * @param string $token Token to use between levels. '--' by default.
568 *
569 * @return array The flattened tree.
570 */
571 private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
572 $result = array();
573 foreach ( $tree as $property => $value ) {
574 $new_key = $prefix . str_replace(
575 '/',
576 '-',
577 strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
578 );
579
580 if ( is_array( $value ) ) {
581 $new_prefix = $new_key . $token;
582 $result = array_merge(
583 $result,
584 self::flatten_tree( $value, $new_prefix, $token )
585 );
586 } else {
587 $result[ $new_key ] = $value;
588 }
589 }
590 return $result;
591 }
592
593 /**
594 * Returns the style property for the given path.
595 *
596 * It also converts CSS Custom Property stored as
597 * "var:preset|color|secondary" to the form
598 * "--wp--preset--color--secondary".
599 *
600 * @param array $styles Styles subtree.
601 * @param array $path Which property to process.
602 *
603 * @return string Style property value.
604 */
605 private static function get_property_value( $styles, $path ) {
606 $value = gutenberg_experimental_get( $styles, $path, '' );
607
608 if ( '' === $value ) {
609 return $value;
610 }
611
612 $prefix = 'var:';
613 $prefix_len = strlen( $prefix );
614 $token_in = '|';
615 $token_out = '--';
616 if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
617 $unwrapped_name = str_replace(
618 $token_in,
619 $token_out,
620 substr( $value, $prefix_len )
621 );
622 $value = "var(--wp--$unwrapped_name)";
623 }
624
625 return $value;
626 }
627
628 /**
629 * Given a context, it extracts the style properties
630 * and adds them to the $declarations array following the format:
631 *
632 * ```php
633 * array(
634 * 'name' => 'property_name',
635 * 'value' => 'property_value,
636 * )
637 * ```
638 *
639 * Note that this modifies the $declarations in place.
640 *
641 * @param array $declarations Holds the existing declarations.
642 * @param array $context Input context to process.
643 * @param array $context_supports Supports information for this context.
644 */
645 private static function compute_style_properties( &$declarations, $context, $context_supports ) {
646 if ( empty( $context['styles'] ) ) {
647 return;
648 }
649
650 foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
651 if ( ! in_array( $name, $context_supports, true ) ) {
652 continue;
653 }
654
655 $value = self::get_property_value( $context['styles'], $metadata['value'] );
656 if ( ! empty( $value ) ) {
657 $kebabcased_name = strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $name ) );
658 $declarations[] = array(
659 'name' => $kebabcased_name,
660 'value' => $value,
661 );
662 }
663 }
664 }
665
666 /**
667 * Given a context, it extracts its presets
668 * and adds them to the given input $stylesheet.
669 *
670 * Note this function modifies $stylesheet in place.
671 *
672 * @param string $stylesheet Input stylesheet to add the presets to.
673 * @param array $context Context to process.
674 * @param string $selector Selector wrapping the classes.
675 */
676 private static function compute_preset_classes( &$stylesheet, $context, $selector ) {
677 if ( self::GLOBAL_SELECTOR === $selector ) {
678 // Classes at the global level do not need any CSS prefixed,
679 // and we don't want to increase its specificity.
680 $selector = '';
681 }
682
683 foreach ( self::PRESETS_METADATA as $preset ) {
684 $values = gutenberg_experimental_get( $context, $preset['path'], array() );
685 foreach ( $values as $value ) {
686 foreach ( $preset['classes'] as $class ) {
687 $stylesheet .= self::to_ruleset(
688 $selector . '.has-' . $value['slug'] . '-' . $class['class_suffix'],
689 array(
690 array(
691 'name' => $class['property_name'],
692 'value' => $value[ $preset['value_key'] ],
693 ),
694 )
695 );
696 }
697 }
698 }
699 }
700
701 /**
702 * Given a context, it extracts the CSS Custom Properties
703 * for the presets and adds them to the $declarations array
704 * following the format:
705 *
706 * ```php
707 * array(
708 * 'name' => 'property_name',
709 * 'value' => 'property_value,
710 * )
711 * ```
712 *
713 * Note that this modifies the $declarations in place.
714 *
715 * @param array $declarations Holds the existing declarations.
716 * @param array $context Input context to process.
717 */
718 private static function compute_preset_vars( &$declarations, $context ) {
719 foreach ( self::PRESETS_METADATA as $preset ) {
720 $values = gutenberg_experimental_get( $context, $preset['path'], array() );
721 foreach ( $values as $value ) {
722 $declarations[] = array(
723 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'],
724 'value' => $value[ $preset['value_key'] ],
725 );
726 }
727 }
728 }
729
730 /**
731 * Given a context, it extracts the CSS Custom Properties
732 * for the custom values and adds them to the $declarations
733 * array following the format:
734 *
735 * ```php
736 * array(
737 * 'name' => 'property_name',
738 * 'value' => 'property_value,
739 * )
740 * ```
741 *
742 * Note that this modifies the $declarations in place.
743 *
744 * @param array $declarations Holds the existing declarations.
745 * @param array $context Input context to process.
746 */
747 private static function compute_theme_vars( &$declarations, $context ) {
748 $custom_values = gutenberg_experimental_get( $context, array( 'settings', 'custom' ) );
749 $css_vars = self::flatten_tree( $custom_values );
750 foreach ( $css_vars as $key => $value ) {
751 $declarations[] = array(
752 'name' => '--wp--custom--' . $key,
753 'value' => $value,
754 );
755 }
756 }
757
758 /**
759 * Given a selector and a declaration list,
760 * creates the corresponding ruleset.
761 *
762 * To help debugging, will add some space
763 * if SCRIPT_DEBUG is defined and true.
764 *
765 * @param string $selector CSS selector.
766 * @param array $declarations List of declarations.
767 *
768 * @return string CSS ruleset.
769 */
770 private static function to_ruleset( $selector, $declarations ) {
771 if ( empty( $declarations ) ) {
772 return '';
773 }
774 $ruleset = '';
775
776 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
777 $declaration_block = array_reduce(
778 $declarations,
779 function ( $carry, $element ) {
780 return $carry .= "\t" . $element['name'] . ': ' . $element['value'] . ";\n"; },
781 ''
782 );
783 $ruleset .= $selector . " {\n" . $declaration_block . "}\n";
784 } else {
785 $declaration_block = array_reduce(
786 $declarations,
787 function ( $carry, $element ) {
788 return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
789 ''
790 );
791 $ruleset .= $selector . '{' . $declaration_block . '}';
792 }
793
794 return $ruleset;
795 }
796
797 /**
798 * Converts each context into a list of rulesets
799 * to be appended to the stylesheet.
800 * These rulesets contain all the css variables (custom variables and preset variables).
801 *
802 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
803 *
804 * For each context this creates a new ruleset such as:
805 *
806 * context-selector {
807 * --wp--preset--category--slug: value;
808 * --wp--custom--variable: value;
809 * }
810 *
811 * @return string The new stylesheet.
812 */
813 private function get_css_variables() {
814 $stylesheet = '';
815 $metadata = $this->get_blocks_metadata();
816 foreach ( $this->contexts as $context_name => $context ) {
817 if ( empty( $metadata[ $context_name ]['selector'] ) ) {
818 continue;
819 }
820 $selector = $metadata[ $context_name ]['selector'];
821
822 $declarations = array();
823 self::compute_preset_vars( $declarations, $context );
824 self::compute_theme_vars( $declarations, $context );
825
826 // Attach the ruleset for style and custom properties.
827 $stylesheet .= self::to_ruleset( $selector, $declarations );
828 }
829 return $stylesheet;
830 }
831
832 /**
833 * Converts each context into a list of rulesets
834 * containing the block styles to be appended to the stylesheet.
835 *
836 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
837 *
838 * For each context this creates a new ruleset such as:
839 *
840 * context-selector {
841 * style-property-one: value;
842 * }
843 *
844 * Additionally, it'll also create new rulesets
845 * as classes for each preset value such as:
846 *
847 * .has-value-color {
848 * color: value;
849 * }
850 *
851 * .has-value-background-color {
852 * background-color: value;
853 * }
854 *
855 * .has-value-font-size {
856 * font-size: value;
857 * }
858 *
859 * .has-value-gradient-background {
860 * background: value;
861 * }
862 *
863 * p.has-value-gradient-background {
864 * background: value;
865 * }
866 *
867 * @return string The new stylesheet.
868 */
869 private function get_block_styles() {
870 $stylesheet = '';
871 $metadata = $this->get_blocks_metadata();
872 foreach ( $this->contexts as $context_name => $context ) {
873 if ( empty( $metadata[ $context_name ]['selector'] ) || empty( $metadata[ $context_name ]['supports'] ) ) {
874 continue;
875 }
876 $selector = $metadata[ $context_name ]['selector'];
877 $supports = $metadata[ $context_name ]['supports'];
878
879 $declarations = array();
880 self::compute_style_properties( $declarations, $context, $supports );
881
882 $stylesheet .= self::to_ruleset( $selector, $declarations );
883
884 // Attach the rulesets for the classes.
885 self::compute_preset_classes( $stylesheet, $context, $selector );
886 }
887
888 return $stylesheet;
889 }
890
891 /**
892 * Returns the existing settings for each context.
893 *
894 * Example:
895 *
896 * {
897 * 'global': {
898 * 'color': {
899 * 'custom': true
900 * }
901 * },
902 * 'core/paragraph': {
903 * 'spacing': {
904 * 'customPadding': true
905 * }
906 * }
907 * }
908 *
909 * @return array Settings per context.
910 */
911 public function get_settings() {
912 return array_filter(
913 array_map( array( $this, 'extract_settings' ), $this->contexts ),
914 function ( $element ) {
915 return null !== $element;
916 }
917 );
918 }
919
920 /**
921 * Returns the stylesheet that results of processing
922 * the theme.json structure this object represents.
923 *
924 * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'.
925 * @return string Stylesheet.
926 */
927 public function get_stylesheet( $type = 'all' ) {
928 switch ( $type ) {
929 case 'block_styles':
930 return $this->get_block_styles();
931 case 'css_variables':
932 return $this->get_css_variables();
933 default:
934 return $this->get_css_variables() . $this->get_block_styles();
935 }
936 }
937
938 /**
939 * Merge new incoming data.
940 *
941 * @param WP_Theme_JSON $theme_json Data to merge.
942 */
943 public function merge( $theme_json ) {
944 $incoming_data = $theme_json->get_raw_data();
945
946 foreach ( array_keys( $incoming_data ) as $context ) {
947 foreach ( array( 'settings', 'styles' ) as $subtree ) {
948 if ( ! isset( $incoming_data[ $context ][ $subtree ] ) ) {
949 continue;
950 }
951
952 if ( ! isset( $this->contexts[ $context ][ $subtree ] ) ) {
953 $this->contexts[ $context ][ $subtree ] = $incoming_data[ $context ][ $subtree ];
954 continue;
955 }
956
957 foreach ( array_keys( self::SCHEMA[ $subtree ] ) as $leaf ) {
958 if ( ! isset( $incoming_data[ $context ][ $subtree ][ $leaf ] ) ) {
959 continue;
960 }
961
962 if ( ! isset( $this->contexts[ $context ][ $subtree ][ $leaf ] ) ) {
963 $this->contexts[ $context ][ $subtree ][ $leaf ] = $incoming_data[ $context ][ $subtree ][ $leaf ];
964 continue;
965 }
966
967 $this->contexts[ $context ][ $subtree ][ $leaf ] = array_merge(
968 $this->contexts[ $context ][ $subtree ][ $leaf ],
969 $incoming_data[ $context ][ $subtree ][ $leaf ]
970 );
971 }
972 }
973 }
974 }
975
976 /**
977 * Retuns the raw data.
978 *
979 * @return array Raw data.
980 */
981 public function get_raw_data() {
982 return $this->contexts;
983 }
984
985 }
986