PluginProbe
Gutenberg / 10.2.1
Gutenberg v10.2.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 10.2.1, at lib/class-wp-theme-json.php

1,232 lines 35.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 $theme_json = 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 * How to address all the blocks
32 * in the theme.json file.
33 */
34 const ALL_BLOCKS_NAME = 'defaults';
35
36 /**
37 * The CSS selector for the * block,
38 * only using to generate presets.
39 *
40 * @var string
41 */
42 const ALL_BLOCKS_SELECTOR = ':root';
43
44 /**
45 * How to address the root block
46 * in the theme.json file.
47 *
48 * @var string
49 */
50 const ROOT_BLOCK_NAME = 'root';
51
52 /**
53 * The CSS selector for the root block.
54 *
55 * @var string
56 */
57 const ROOT_BLOCK_SELECTOR = ':root';
58
59 /**
60 * The supported properties of the root block.
61 *
62 * @var array
63 */
64 const ROOT_BLOCK_SUPPORTS = array(
65 '--wp--style--color--link',
66 'background',
67 'backgroundColor',
68 'color',
69 'fontFamily',
70 'fontSize',
71 'fontStyle',
72 'fontWeight',
73 'lineHeight',
74 'textDecoration',
75 'textTransform',
76 );
77
78 /**
79 * Data schema of each block within a theme.json.
80 *
81 * Example:
82 *
83 * {
84 * 'block-one': {
85 * 'styles': {
86 * 'color': {
87 * 'background': 'color'
88 * }
89 * },
90 * 'settings': {
91 * 'color': {
92 * 'custom': true
93 * }
94 * }
95 * },
96 * 'block-two': {
97 * 'styles': {
98 * 'color': {
99 * 'link': 'color'
100 * }
101 * }
102 * }
103 * }
104 */
105 const SCHEMA = array(
106 'customTemplates' => null,
107 'templateParts' => null,
108 'styles' => array(
109 'border' => array(
110 'radius' => null,
111 'color' => null,
112 'style' => null,
113 'width' => null,
114 ),
115 'color' => array(
116 'background' => null,
117 'gradient' => null,
118 'link' => null,
119 'text' => null,
120 ),
121 'spacing' => array(
122 'padding' => array(
123 'top' => null,
124 'right' => null,
125 'bottom' => null,
126 'left' => null,
127 ),
128 ),
129 'typography' => array(
130 'fontFamily' => null,
131 'fontSize' => null,
132 'fontStyle' => null,
133 'fontWeight' => null,
134 'lineHeight' => null,
135 'textDecoration' => null,
136 'textTransform' => null,
137 ),
138 ),
139 'settings' => array(
140 'border' => array(
141 'customRadius' => null,
142 'customColor' => null,
143 'customStyle' => null,
144 'customWidth' => null,
145 ),
146 'color' => array(
147 'custom' => null,
148 'customGradient' => null,
149 'gradients' => null,
150 'link' => null,
151 'palette' => null,
152 ),
153 'spacing' => array(
154 'customPadding' => null,
155 'units' => null,
156 ),
157 'typography' => array(
158 'customFontSize' => null,
159 'customLineHeight' => null,
160 'dropCap' => null,
161 'fontFamilies' => null,
162 'fontSizes' => null,
163 'customFontStyle' => null,
164 'customFontWeight' => null,
165 'customTextDecorations' => null,
166 'customTextTransforms' => null,
167 ),
168 'custom' => null,
169 ),
170 );
171
172 /**
173 * Presets are a set of values that serve
174 * to bootstrap some styles: colors, font sizes, etc.
175 *
176 * They are a unkeyed array of values such as:
177 *
178 * ```php
179 * array(
180 * array(
181 * 'slug' => 'unique-name-within-the-set',
182 * 'name' => 'Name for the UI',
183 * <value_key> => 'value'
184 * ),
185 * )
186 * ```
187 *
188 * This contains the necessary metadata to process them:
189 *
190 * - path => where to find the preset within the settings section
191 *
192 * - value_key => the key that represents the value
193 *
194 * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
195 * --wp--preset--<preset_infix>--<slug>: <preset_value>
196 *
197 * - classes => array containing a structure with the classes to
198 * generate for the presets. Each class should have
199 * the class suffix and the property name. Example:
200 *
201 * .has-<slug>-<class_suffix> {
202 * <property_name>: <preset_value>
203 * }
204 */
205 const PRESETS_METADATA = array(
206 array(
207 'path' => array( 'color', 'palette' ),
208 'value_key' => 'color',
209 'css_var_infix' => 'color',
210 'classes' => array(
211 array(
212 'class_suffix' => 'color',
213 'property_name' => 'color',
214 ),
215 array(
216 'class_suffix' => 'background-color',
217 'property_name' => 'background-color',
218 ),
219 ),
220 ),
221 array(
222 'path' => array( 'color', 'gradients' ),
223 'value_key' => 'gradient',
224 'css_var_infix' => 'gradient',
225 'classes' => array(
226 array(
227 'class_suffix' => 'gradient-background',
228 'property_name' => 'background',
229 ),
230 ),
231 ),
232 array(
233 'path' => array( 'typography', 'fontSizes' ),
234 'value_key' => 'size',
235 'css_var_infix' => 'font-size',
236 'classes' => array(
237 array(
238 'class_suffix' => 'font-size',
239 'property_name' => 'font-size',
240 ),
241 ),
242 ),
243 array(
244 'path' => array( 'typography', 'fontFamilies' ),
245 'value_key' => 'fontFamily',
246 'css_var_infix' => 'font-family',
247 'classes' => array(),
248 ),
249 );
250
251 /**
252 * Metadata for style properties.
253 *
254 * Each property declares:
255 *
256 * - 'value': path to the value in theme.json and block attributes.
257 * - 'support': path to the block support in block.json.
258 */
259 const PROPERTIES_METADATA = array(
260 '--wp--style--color--link' => array(
261 'value' => array( 'color', 'link' ),
262 'support' => array( 'color', 'link' ),
263 ),
264 'background' => array(
265 'value' => array( 'color', 'gradient' ),
266 'support' => array( 'color', 'gradients' ),
267 ),
268 'backgroundColor' => array(
269 'value' => array( 'color', 'background' ),
270 'support' => array( 'color' ),
271 ),
272 'borderRadius' => array(
273 'value' => array( 'border', 'radius' ),
274 'support' => array( '__experimentalBorder', 'radius' ),
275 ),
276 'borderColor' => array(
277 'value' => array( 'border', 'color' ),
278 'support' => array( '__experimentalBorder', 'color' ),
279 ),
280 'borderWidth' => array(
281 'value' => array( 'border', 'width' ),
282 'support' => array( '__experimentalBorder', 'width' ),
283 ),
284 'borderStyle' => array(
285 'value' => array( 'border', 'style' ),
286 'support' => array( '__experimentalBorder', 'style' ),
287 ),
288 'color' => array(
289 'value' => array( 'color', 'text' ),
290 'support' => array( 'color' ),
291 ),
292 'fontFamily' => array(
293 'value' => array( 'typography', 'fontFamily' ),
294 'support' => array( '__experimentalFontFamily' ),
295 ),
296 'fontSize' => array(
297 'value' => array( 'typography', 'fontSize' ),
298 'support' => array( 'fontSize' ),
299 ),
300 'fontStyle' => array(
301 'value' => array( 'typography', 'fontStyle' ),
302 'support' => array( '__experimentalFontStyle' ),
303 ),
304 'fontWeight' => array(
305 'value' => array( 'typography', 'fontWeight' ),
306 'support' => array( '__experimentalFontWeight' ),
307 ),
308 'lineHeight' => array(
309 'value' => array( 'typography', 'lineHeight' ),
310 'support' => array( 'lineHeight' ),
311 ),
312 'padding' => array(
313 'value' => array( 'spacing', 'padding' ),
314 'support' => array( 'spacing', 'padding' ),
315 'properties' => array( 'top', 'right', 'bottom', 'left' ),
316 ),
317 'textDecoration' => array(
318 'value' => array( 'typography', 'textDecoration' ),
319 'support' => array( '__experimentalTextDecoration' ),
320 ),
321 'textTransform' => array(
322 'value' => array( 'typography', 'textTransform' ),
323 'support' => array( '__experimentalTextTransform' ),
324 ),
325 );
326
327 /**
328 * Constructor.
329 *
330 * @param array $theme_json A structure that follows the theme.json schema.
331 */
332 public function __construct( $theme_json = array() ) {
333 $this->theme_json = array();
334
335 if ( ! is_array( $theme_json ) ) {
336 return;
337 }
338
339 // Remove top-level keys that aren't present in the schema.
340 $this->theme_json = array_intersect_key( $theme_json, self::SCHEMA );
341
342 $block_metadata = self::get_blocks_metadata();
343 foreach ( array( 'settings', 'styles' ) as $subtree ) {
344 // Remove settings & styles subtrees if they aren't arrays.
345 if ( isset( $this->theme_json[ $subtree ] ) && ! is_array( $this->theme_json[ $subtree ] ) ) {
346 unset( $this->theme_json[ $subtree ] );
347 }
348
349 // Remove block selectors subtrees declared within settings & styles if that aren't registered.
350 if ( isset( $this->theme_json[ $subtree ] ) ) {
351 $this->theme_json[ $subtree ] = array_intersect_key( $this->theme_json[ $subtree ], $block_metadata );
352 }
353 }
354
355 foreach ( $block_metadata as $block_selector => $metadata ) {
356 if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) {
357 // Remove the block selector subtree if it's not an array.
358 if ( ! is_array( $this->theme_json['styles'][ $block_selector ] ) ) {
359 unset( $this->theme_json['styles'][ $block_selector ] );
360 continue;
361 }
362
363 // Remove the properties the block doesn't support.
364 // This is a subset of the full styles schema.
365 $styles_schema = self::SCHEMA['styles'];
366 foreach ( self::PROPERTIES_METADATA as $prop_name => $prop_meta ) {
367 if ( ! in_array( $prop_name, $metadata['supports'], true ) ) {
368 unset( $styles_schema[ $prop_meta['value'][0] ][ $prop_meta['value'][1] ] );
369 }
370 }
371 $this->theme_json['styles'][ $block_selector ] = self::remove_keys_not_in_schema(
372 $this->theme_json['styles'][ $block_selector ],
373 $styles_schema
374 );
375
376 // Remove the block selector subtree if it is empty after having processed it.
377 if ( empty( $this->theme_json['styles'][ $block_selector ] ) ) {
378 unset( $this->theme_json['styles'][ $block_selector ] );
379 }
380 }
381
382 if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) {
383 // Remove the block selector subtree if it's not an array.
384 if ( ! is_array( $this->theme_json['settings'][ $block_selector ] ) ) {
385 unset( $this->theme_json['settings'][ $block_selector ] );
386 continue;
387 }
388
389 // Remove the properties that aren't present in the schema.
390 $this->theme_json['settings'][ $block_selector ] = self::remove_keys_not_in_schema(
391 $this->theme_json['settings'][ $block_selector ],
392 self::SCHEMA['settings']
393 );
394
395 // Remove the block selector subtree if it is empty after having processed it.
396 if ( empty( $this->theme_json['settings'][ $block_selector ] ) ) {
397 unset( $this->theme_json['settings'][ $block_selector ] );
398 }
399 }
400 }
401
402 // Remove the settings & styles subtrees if they're empty after having processed them.
403 foreach ( array( 'settings', 'styles' ) as $subtree ) {
404 if ( empty( $this->theme_json[ $subtree ] ) ) {
405 unset( $this->theme_json[ $subtree ] );
406 }
407 }
408
409 }
410
411 /**
412 * Returns the kebab-cased name of a given property.
413 *
414 * @param string $property Property name to convert.
415 * @return string kebab-cased name of the property
416 */
417 private static function to_kebab_case( $property ) {
418 $mappings = self::get_case_mappings();
419 return $mappings['to_kebab_case'][ $property ];
420 }
421
422 /**
423 * Returns the property name of a kebab-cased property.
424 *
425 * @param string $property Property name to convert in kebab-case.
426 * @return string Name of the property
427 */
428 private static function to_property( $property ) {
429 $mappings = self::get_case_mappings();
430 return $mappings['to_property'][ $property ];
431 }
432
433 /**
434 * Returns a mapping on metadata properties to avoid having to constantly
435 * transforms properties between camel case and kebab.
436 *
437 * @return array Containing two mappings:
438 *
439 * - "to_kebab_case" mapping properties in camel case to
440 * properties in kebab case e.g: "paddingTop" to "padding-top".
441 *
442 * - "to_property" mapping properties in kebab case to
443 * the main properties in camel case e.g: "padding-top" to "padding".
444 */
445 private static function get_case_mappings() {
446 static $case_mappings;
447 if ( null === $case_mappings ) {
448 $case_mappings = array(
449 'to_kebab_case' => array(),
450 'to_property' => array(),
451 );
452 foreach ( self::PROPERTIES_METADATA as $key => $metadata ) {
453 $kebab_case = strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $key ) );
454
455 $case_mappings['to_kebab_case'][ $key ] = $kebab_case;
456 $case_mappings['to_property'][ $kebab_case ] = $key;
457 if ( self::has_properties( $metadata ) ) {
458 foreach ( $metadata['properties'] as $property ) {
459 $camel_case = $key . ucfirst( $property );
460 $kebab_case = strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $camel_case ) );
461
462 $case_mappings['to_kebab_case'][ $camel_case ] = $kebab_case;
463 $case_mappings['to_property'][ $kebab_case ] = $key;
464 }
465 }
466 }
467 }
468 return $case_mappings;
469 }
470
471 /**
472 * Returns the metadata for each block.
473 *
474 * Example:
475 *
476 * {
477 * 'root': {
478 * 'selector': ':root'
479 * 'supports': [ 'fontSize', 'backgroundColor' ],
480 * },
481 * 'core/heading/h1': {
482 * 'selector': 'h1'
483 * 'supports': [ 'fontSize', 'backgroundColor' ],
484 * }
485 * }
486 *
487 * @return array Block metadata.
488 */
489 private static function get_blocks_metadata() {
490 if ( null !== self::$blocks_metadata ) {
491 return self::$blocks_metadata;
492 }
493
494 self::$blocks_metadata = array(
495 self::ROOT_BLOCK_NAME => array(
496 'selector' => self::ROOT_BLOCK_SELECTOR,
497 'supports' => self::ROOT_BLOCK_SUPPORTS,
498 ),
499 // By make supports an empty array
500 // this won't have any styles associated
501 // but still allows adding settings
502 // and generate presets.
503 self::ALL_BLOCKS_NAME => array(
504 'selector' => self::ALL_BLOCKS_SELECTOR,
505 'supports' => array(),
506 ),
507 );
508
509 $registry = WP_Block_Type_Registry::get_instance();
510 $blocks = $registry->get_all_registered();
511 foreach ( $blocks as $block_name => $block_type ) {
512 /*
513 * Extract block support keys that are related to the style properties.
514 */
515 $block_supports = array();
516 foreach ( self::PROPERTIES_METADATA as $key => $metadata ) {
517 if ( _wp_array_get( $block_type->supports, $metadata['support'] ) ) {
518 $block_supports[] = $key;
519 }
520 }
521
522 /*
523 * Assign the selector for the block.
524 *
525 * Some blocks can declare multiple selectors:
526 *
527 * - core/heading represents the H1-H6 HTML elements
528 * - core/list represents the UL and OL HTML elements
529 * - core/group is meant to represent DIV and other HTML elements
530 *
531 * Some other blocks don't provide a selector,
532 * so we generate a class for them based on their name:
533 *
534 * - 'core/group' => '.wp-block-group'
535 * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name'
536 *
537 * Note that, for core blocks, we don't add the `core/` prefix to its class name.
538 * This is for historical reasons, as they come with a class without that infix.
539 *
540 */
541 if (
542 isset( $block_type->supports['__experimentalSelector'] ) &&
543 is_string( $block_type->supports['__experimentalSelector'] )
544 ) {
545 self::$blocks_metadata[ $block_name ] = array(
546 'selector' => $block_type->supports['__experimentalSelector'],
547 'supports' => $block_supports,
548 );
549 } elseif (
550 isset( $block_type->supports['__experimentalSelector'] ) &&
551 is_array( $block_type->supports['__experimentalSelector'] )
552 ) {
553 foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector_metadata ) {
554 if ( ! isset( $selector_metadata['selector'] ) ) {
555 continue;
556 }
557
558 self::$blocks_metadata[ $key ] = array(
559 'selector' => $selector_metadata['selector'],
560 'supports' => $block_supports,
561 );
562 }
563 } else {
564 self::$blocks_metadata[ $block_name ] = array(
565 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ),
566 'supports' => $block_supports,
567 );
568 }
569 }
570
571 return self::$blocks_metadata;
572 }
573
574 /**
575 * Given a tree, removes the keys that are not present in the schema.
576 *
577 * It is recursive and modifies the input in-place.
578 *
579 * @param array $tree Input to process.
580 * @param array $schema Schema to adhere to.
581 *
582 * @return array Returns the modified $tree.
583 */
584 private static function remove_keys_not_in_schema( $tree, $schema ) {
585 $tree = array_intersect_key( $tree, $schema );
586
587 foreach ( $schema as $key => $data ) {
588 if ( is_array( $schema[ $key ] ) && isset( $tree[ $key ] ) ) {
589 $tree[ $key ] = self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
590
591 if ( empty( $tree[ $key ] ) ) {
592 unset( $tree[ $key ] );
593 }
594 }
595 }
596
597 return $tree;
598 }
599
600 /**
601 * Given a tree, it creates a flattened one
602 * by merging the keys and binding the leaf values
603 * to the new keys.
604 *
605 * It also transforms camelCase names into kebab-case
606 * and substitutes '/' by '-'.
607 *
608 * This is thought to be useful to generate
609 * CSS Custom Properties from a tree,
610 * although there's nothing in the implementation
611 * of this function that requires that format.
612 *
613 * For example, assuming the given prefix is '--wp'
614 * and the token is '--', for this input tree:
615 *
616 * {
617 * 'some/property': 'value',
618 * 'nestedProperty': {
619 * 'sub-property': 'value'
620 * }
621 * }
622 *
623 * it'll return this output:
624 *
625 * {
626 * '--wp--some-property': 'value',
627 * '--wp--nested-property--sub-property': 'value'
628 * }
629 *
630 * @param array $tree Input tree to process.
631 * @param string $prefix Prefix to prepend to each variable. '' by default.
632 * @param string $token Token to use between levels. '--' by default.
633 *
634 * @return array The flattened tree.
635 */
636 private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
637 $result = array();
638 foreach ( $tree as $property => $value ) {
639 $new_key = $prefix . str_replace(
640 '/',
641 '-',
642 strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
643 );
644
645 if ( is_array( $value ) ) {
646 $new_prefix = $new_key . $token;
647 $result = array_merge(
648 $result,
649 self::flatten_tree( $value, $new_prefix, $token )
650 );
651 } else {
652 $result[ $new_key ] = $value;
653 }
654 }
655 return $result;
656 }
657
658 /**
659 * Returns the style property for the given path.
660 *
661 * It also converts CSS Custom Property stored as
662 * "var:preset|color|secondary" to the form
663 * "--wp--preset--color--secondary".
664 *
665 * @param array $styles Styles subtree.
666 * @param array $path Which property to process.
667 *
668 * @return string Style property value.
669 */
670 private static function get_property_value( $styles, $path ) {
671 $value = _wp_array_get( $styles, $path, '' );
672
673 if ( '' === $value ) {
674 return $value;
675 }
676
677 $prefix = 'var:';
678 $prefix_len = strlen( $prefix );
679 $token_in = '|';
680 $token_out = '--';
681 if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
682 $unwrapped_name = str_replace(
683 $token_in,
684 $token_out,
685 substr( $value, $prefix_len )
686 );
687 $value = "var(--wp--$unwrapped_name)";
688 }
689
690 return $value;
691 }
692
693 /**
694 * Whether the metadata contains a key named properties.
695 *
696 * @param array $metadata Description of the style property.
697 *
698 * @return boolean True if properties exists, false otherwise.
699 */
700 private static function has_properties( $metadata ) {
701 if ( array_key_exists( 'properties', $metadata ) ) {
702 return true;
703 }
704
705 return false;
706 }
707
708 /**
709 * Given a styles array, it extracts the style properties
710 * and adds them to the $declarations array following the format:
711 *
712 * ```php
713 * array(
714 * 'name' => 'property_name',
715 * 'value' => 'property_value,
716 * )
717 * ```
718 *
719 * @param array $declarations Holds the existing declarations.
720 * @param array $styles Styles to process.
721 * @param array $supports Supports information for this block.
722 *
723 * @return array Returns the modified $declarations.
724 */
725 private static function compute_style_properties( $declarations, $styles, $supports ) {
726 if ( empty( $styles ) ) {
727 return $declarations;
728 }
729
730 $properties = array();
731 foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
732 if ( ! in_array( $name, $supports, true ) ) {
733 continue;
734 }
735
736 // Some properties can be shorthand properties, meaning that
737 // they contain multiple values instead of a single one.
738 // An example of this is the padding property, see self::SCHEMA.
739 if ( self::has_properties( $metadata ) ) {
740 foreach ( $metadata['properties'] as $property ) {
741 $properties[] = array(
742 'name' => $name . ucfirst( $property ),
743 'value' => array_merge( $metadata['value'], array( $property ) ),
744 );
745 }
746 } else {
747 $properties[] = array(
748 'name' => $name,
749 'value' => $metadata['value'],
750 );
751 }
752 }
753
754 foreach ( $properties as $prop ) {
755 $value = self::get_property_value( $styles, $prop['value'] );
756 if ( ! empty( $value ) ) {
757 $kebab_cased_name = self::to_kebab_case( $prop['name'] );
758 $declarations[] = array(
759 'name' => $kebab_cased_name,
760 'value' => $value,
761 );
762 }
763 }
764
765 return $declarations;
766 }
767
768 /**
769 * Given a settings array, it returns the generated rulesets
770 * for the preset classes.
771 *
772 * @param array $settings Settings to process.
773 * @param string $selector Selector wrapping the classes.
774 *
775 * @return string The result of processing the presets.
776 */
777 private static function compute_preset_classes( $settings, $selector ) {
778 if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
779 // Classes at the global level do not need any CSS prefixed,
780 // and we don't want to increase its specificity.
781 $selector = '';
782 }
783
784 $stylesheet = '';
785 foreach ( self::PRESETS_METADATA as $preset ) {
786 $values = _wp_array_get( $settings, $preset['path'], array() );
787 foreach ( $values as $value ) {
788 foreach ( $preset['classes'] as $class ) {
789 $stylesheet .= self::to_ruleset(
790 $selector . '.has-' . $value['slug'] . '-' . $class['class_suffix'],
791 array(
792 array(
793 'name' => $class['property_name'],
794 'value' => $value[ $preset['value_key'] ] . ' !important',
795 ),
796 )
797 );
798 }
799 }
800 }
801
802 return $stylesheet;
803 }
804
805 /**
806 * Given the block settings, it extracts the CSS Custom Properties
807 * for the presets and adds them to the $declarations array
808 * following the format:
809 *
810 * ```php
811 * array(
812 * 'name' => 'property_name',
813 * 'value' => 'property_value,
814 * )
815 * ```
816 *
817 * @param array $declarations Holds the existing declarations.
818 * @param array $settings Settings to process.
819 *
820 * @return array Returns the modified $declarations.
821 */
822 private static function compute_preset_vars( $declarations, $settings ) {
823 foreach ( self::PRESETS_METADATA as $preset ) {
824 $values = _wp_array_get( $settings, $preset['path'], array() );
825 foreach ( $values as $value ) {
826 $declarations[] = array(
827 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'],
828 'value' => $value[ $preset['value_key'] ],
829 );
830 }
831 }
832
833 return $declarations;
834 }
835
836 /**
837 * Given an array of settings, it extracts the CSS Custom Properties
838 * for the custom values and adds them to the $declarations
839 * array following the format:
840 *
841 * ```php
842 * array(
843 * 'name' => 'property_name',
844 * 'value' => 'property_value,
845 * )
846 * ```
847 *
848 * @param array $declarations Holds the existing declarations.
849 * @param array $settings Settings to process.
850 *
851 * @return array Returns the modified $declarations.
852 */
853 private static function compute_theme_vars( $declarations, $settings ) {
854 $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
855 $css_vars = self::flatten_tree( $custom_values );
856 foreach ( $css_vars as $key => $value ) {
857 $declarations[] = array(
858 'name' => '--wp--custom--' . $key,
859 'value' => $value,
860 );
861 }
862
863 return $declarations;
864 }
865
866 /**
867 * Given a selector and a declaration list,
868 * creates the corresponding ruleset.
869 *
870 * To help debugging, will add some space
871 * if SCRIPT_DEBUG is defined and true.
872 *
873 * @param string $selector CSS selector.
874 * @param array $declarations List of declarations.
875 *
876 * @return string CSS ruleset.
877 */
878 private static function to_ruleset( $selector, $declarations ) {
879 if ( empty( $declarations ) ) {
880 return '';
881 }
882 $ruleset = '';
883
884 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
885 $declaration_block = array_reduce(
886 $declarations,
887 function ( $carry, $element ) {
888 return $carry .= "\t" . $element['name'] . ': ' . $element['value'] . ";\n"; },
889 ''
890 );
891 $ruleset .= $selector . " {\n" . $declaration_block . "}\n";
892 } else {
893 $declaration_block = array_reduce(
894 $declarations,
895 function ( $carry, $element ) {
896 return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
897 ''
898 );
899 $ruleset .= $selector . '{' . $declaration_block . '}';
900 }
901
902 return $ruleset;
903 }
904
905 /**
906 * Converts each styles section into a list of rulesets
907 * to be appended to the stylesheet.
908 * These rulesets contain all the css variables (custom variables and preset variables).
909 *
910 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
911 *
912 * For each section this creates a new ruleset such as:
913 *
914 * block-selector {
915 * --wp--preset--category--slug: value;
916 * --wp--custom--variable: value;
917 * }
918 *
919 * @return string The new stylesheet.
920 */
921 private function get_css_variables() {
922 $stylesheet = '';
923 if ( ! isset( $this->theme_json['settings'] ) ) {
924 return $stylesheet;
925 }
926
927 $metadata = self::get_blocks_metadata();
928 foreach ( $this->theme_json['settings'] as $block_selector => $settings ) {
929 if ( empty( $metadata[ $block_selector ]['selector'] ) ) {
930 continue;
931 }
932 $selector = $metadata[ $block_selector ]['selector'];
933
934 $declarations = self::compute_preset_vars( array(), $settings );
935 $declarations = self::compute_theme_vars( $declarations, $settings );
936
937 // Attach the ruleset for style and custom properties.
938 $stylesheet .= self::to_ruleset( $selector, $declarations );
939 }
940 return $stylesheet;
941 }
942
943 /**
944 * Converts each style section into a list of rulesets
945 * containing the block styles to be appended to the stylesheet.
946 *
947 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
948 *
949 * For each section this creates a new ruleset such as:
950 *
951 * block-selector {
952 * style-property-one: value;
953 * }
954 *
955 * Additionally, it'll also create new rulesets
956 * as classes for each preset value such as:
957 *
958 * .has-value-color {
959 * color: value;
960 * }
961 *
962 * .has-value-background-color {
963 * background-color: value;
964 * }
965 *
966 * .has-value-font-size {
967 * font-size: value;
968 * }
969 *
970 * .has-value-gradient-background {
971 * background: value;
972 * }
973 *
974 * p.has-value-gradient-background {
975 * background: value;
976 * }
977 *
978 * @return string The new stylesheet.
979 */
980 private function get_block_styles() {
981 $stylesheet = '';
982 if ( ! isset( $this->theme_json['styles'] ) && ! isset( $this->theme_json['settings'] ) ) {
983 return $stylesheet;
984 }
985
986 $metadata = self::get_blocks_metadata();
987 $block_rules = '';
988 $preset_rules = '';
989 foreach ( $metadata as $block_selector => $metadata ) {
990 if ( empty( $metadata['selector'] ) ) {
991 continue;
992 }
993
994 $selector = $metadata['selector'];
995 $supports = $metadata['supports'];
996
997 $declarations = array();
998 if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) {
999 $declarations = self::compute_style_properties(
1000 $declarations,
1001 $this->theme_json['styles'][ $block_selector ],
1002 $supports
1003 );
1004 }
1005
1006 $block_rules .= self::to_ruleset( $selector, $declarations );
1007
1008 // Attach the rulesets for the classes.
1009 if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) {
1010 $preset_rules .= self::compute_preset_classes(
1011 $this->theme_json['settings'][ $block_selector ],
1012 $selector
1013 );
1014 }
1015 }
1016
1017 return $block_rules . $preset_rules;
1018 }
1019
1020 /**
1021 * Returns the existing settings for each block.
1022 *
1023 * Example:
1024 *
1025 * {
1026 * 'root': {
1027 * 'color': {
1028 * 'custom': true
1029 * }
1030 * },
1031 * 'core/paragraph': {
1032 * 'spacing': {
1033 * 'customPadding': true
1034 * }
1035 * }
1036 * }
1037 *
1038 * @return array Settings per block.
1039 */
1040 public function get_settings() {
1041 if ( ! isset( $this->theme_json['settings'] ) ) {
1042 return array();
1043 } else {
1044 return $this->theme_json['settings'];
1045 }
1046 }
1047
1048 /**
1049 * Returns the page templates of the current theme.
1050 *
1051 * @return array
1052 */
1053 public function get_custom_templates() {
1054 if ( ! isset( $this->theme_json['customTemplates'] ) ) {
1055 return array();
1056 } else {
1057 return $this->theme_json['customTemplates'];
1058 }
1059 }
1060
1061 /**
1062 * Returns the template part data of current theme.
1063 *
1064 * @return array
1065 */
1066 public function get_template_parts() {
1067 if ( ! isset( $this->theme_json['templateParts'] ) ) {
1068 return array();
1069 }
1070 return $this->theme_json['templateParts'];
1071 }
1072
1073 /**
1074 * Returns the stylesheet that results of processing
1075 * the theme.json structure this object represents.
1076 *
1077 * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'.
1078 * @return string Stylesheet.
1079 */
1080 public function get_stylesheet( $type = 'all' ) {
1081 switch ( $type ) {
1082 case 'block_styles':
1083 return $this->get_block_styles();
1084 case 'css_variables':
1085 return $this->get_css_variables();
1086 default:
1087 return $this->get_css_variables() . $this->get_block_styles();
1088 }
1089 }
1090
1091 /**
1092 * Merge new incoming data.
1093 *
1094 * @param WP_Theme_JSON $incoming Data to merge.
1095 */
1096 public function merge( $incoming ) {
1097 $incoming_data = $incoming->get_raw_data();
1098 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
1099
1100 // The array_replace_recursive algorithm merges at the leaf level.
1101 // This means that when a leaf value is an array,
1102 // the incoming array won't replace the existing,
1103 // but the numeric indexes are used for replacement.
1104 //
1105 // These are the cases that have array values at the leaf levels.
1106 $block_metadata = self::get_blocks_metadata();
1107 foreach ( $block_metadata as $block_selector => $meta ) {
1108 // Color presets: palette & gradients.
1109 if ( isset( $incoming_data['settings'][ $block_selector ]['color']['palette'] ) ) {
1110 $this->theme_json['settings'][ $block_selector ]['color']['palette'] = $incoming_data['settings'][ $block_selector ]['color']['palette'];
1111 }
1112 if ( isset( $incoming_data['settings'][ $block_selector ]['color']['gradients'] ) ) {
1113 $this->theme_json['settings'][ $block_selector ]['color']['gradients'] = $incoming_data['settings'][ $block_selector ]['color']['gradients'];
1114 }
1115 // Spacing: units.
1116 if ( isset( $incoming_data['settings'][ $block_selector ]['spacing']['units'] ) ) {
1117 $this->theme_json['settings'][ $block_selector ]['spacing']['units'] = $incoming_data['settings'][ $block_selector ]['spacing']['units'];
1118 }
1119 // Typography presets: fontSizes & fontFamilies.
1120 if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontSizes'] ) ) {
1121 $this->theme_json['settings'][ $block_selector ]['typography']['fontSizes'] = $incoming_data['settings'][ $block_selector ]['typography']['fontSizes'];
1122 }
1123 if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies'] ) ) {
1124 $this->theme_json['settings'][ $block_selector ]['typography']['fontFamilies'] = $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies'];
1125 }
1126 // Custom section.
1127 if ( isset( $incoming_data['settings'][ $block_selector ]['custom'] ) ) {
1128 $this->theme_json['settings'][ $block_selector ]['custom'] = $incoming_data['settings'][ $block_selector ]['custom'];
1129 }
1130 }
1131 }
1132
1133 /**
1134 * Removes insecure data from theme.json.
1135 */
1136 public function remove_insecure_properties() {
1137 $blocks_metadata = self::get_blocks_metadata();
1138 foreach ( $blocks_metadata as $block_selector => $metadata ) {
1139 $escaped_settings = array();
1140 $escaped_styles = array();
1141
1142 // Style escaping.
1143 if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) {
1144 $declarations = self::compute_style_properties( array(), $this->theme_json['styles'][ $block_selector ], $metadata['supports'] );
1145 foreach ( $declarations as $declaration ) {
1146 $style_to_validate = $declaration['name'] . ': ' . $declaration['value'];
1147 if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) {
1148 $property = self::to_property( $declaration['name'] );
1149 $path = self::PROPERTIES_METADATA[ $property ]['value'];
1150 if ( self::has_properties( self::PROPERTIES_METADATA[ $property ] ) ) {
1151 $declaration_divided = explode( '-', $declaration['name'] );
1152 $path[] = $declaration_divided[1];
1153 }
1154 gutenberg_experimental_set(
1155 $escaped_styles,
1156 $path,
1157 _wp_array_get( $this->theme_json['styles'][ $block_selector ], $path, array() )
1158 );
1159 }
1160 }
1161 }
1162
1163 // Settings escaping.
1164 // For now the ony allowed settings are presets.
1165 if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) {
1166 foreach ( self::PRESETS_METADATA as $preset_metadata ) {
1167 $current_preset = _wp_array_get(
1168 $this->theme_json['settings'][ $block_selector ],
1169 $preset_metadata['path'],
1170 null
1171 );
1172 if ( null !== $current_preset ) {
1173 $escaped_preset = array();
1174 foreach ( $current_preset as $single_preset ) {
1175 if (
1176 esc_attr( esc_html( $single_preset['name'] ) ) === $single_preset['name'] &&
1177 sanitize_html_class( $single_preset['slug'] ) === $single_preset['slug']
1178 ) {
1179 $value = $single_preset[ $preset_metadata['value_key'] ];
1180 $single_preset_is_valid = null;
1181 if ( isset( $preset_metadata['classes'] ) && count( $preset_metadata['classes'] ) > 0 ) {
1182 $single_preset_is_valid = true;
1183 foreach ( $preset_metadata['classes'] as $class_meta_data ) {
1184 $property = $class_meta_data['property_name'];
1185 $style_to_validate = $property . ': ' . $value;
1186 if ( esc_html( safecss_filter_attr( $style_to_validate ) ) !== $style_to_validate ) {
1187 $single_preset_is_valid = false;
1188 break;
1189 }
1190 }
1191 } else {
1192 $property = $preset_metadata['css_var_infix'];
1193 $style_to_validate = $property . ': ' . $value;
1194 $single_preset_is_valid = esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate;
1195 }
1196 if ( $single_preset_is_valid ) {
1197 $escaped_preset[] = $single_preset;
1198 }
1199 }
1200 }
1201 if ( ! empty( $escaped_preset ) ) {
1202 gutenberg_experimental_set( $escaped_settings, $preset_metadata['path'], $escaped_preset );
1203 }
1204 }
1205 }
1206 }
1207
1208 if ( empty( $escaped_settings ) ) {
1209 unset( $this->theme_json['settings'][ $block_selector ] );
1210 } else {
1211 $this->theme_json['settings'][ $block_selector ] = $escaped_settings;
1212 }
1213
1214 if ( empty( $escaped_styles ) ) {
1215 unset( $this->theme_json['styles'][ $block_selector ] );
1216 } else {
1217 $this->theme_json['styles'][ $block_selector ] = $escaped_styles;
1218 }
1219 }
1220 }
1221
1222 /**
1223 * Returns the raw data.
1224 *
1225 * @return array Raw data.
1226 */
1227 public function get_raw_data() {
1228 return $this->theme_json;
1229 }
1230
1231 }
1232