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

1,250 lines 35.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 $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 * Skips blocks that don't declare support,
514 * they don't generate styles.
515 */
516 if (
517 ! property_exists( $block_type, 'supports' ) ||
518 ! is_array( $block_type->supports ) ||
519 empty( $block_type->supports )
520 ) {
521 continue;
522 }
523
524 /*
525 * Extract block support keys that are related to the style properties.
526 */
527 $block_supports = array();
528 foreach ( self::PROPERTIES_METADATA as $key => $metadata ) {
529 if ( gutenberg_experimental_get( $block_type->supports, $metadata['support'] ) ) {
530 $block_supports[] = $key;
531 }
532 }
533
534 /*
535 * Skip blocks that don't support anything related to styles.
536 */
537 if ( empty( $block_supports ) ) {
538 continue;
539 }
540
541 /*
542 * Assign the selector for the block.
543 *
544 * Some blocks can declare multiple selectors:
545 *
546 * - core/heading represents the H1-H6 HTML elements
547 * - core/list represents the UL and OL HTML elements
548 * - core/group is meant to represent DIV and other HTML elements
549 *
550 * Some other blocks don't provide a selector,
551 * so we generate a class for them based on their name:
552 *
553 * - 'core/group' => '.wp-block-group'
554 * - 'my-custom-library/block-name' => '.wp-block-my-custom-library-block-name'
555 *
556 * Note that, for core blocks, we don't add the `core/` prefix to its class name.
557 * This is for historical reasons, as they come with a class without that infix.
558 *
559 */
560 if (
561 isset( $block_type->supports['__experimentalSelector'] ) &&
562 is_string( $block_type->supports['__experimentalSelector'] )
563 ) {
564 self::$blocks_metadata[ $block_name ] = array(
565 'selector' => $block_type->supports['__experimentalSelector'],
566 'supports' => $block_supports,
567 );
568 } elseif (
569 isset( $block_type->supports['__experimentalSelector'] ) &&
570 is_array( $block_type->supports['__experimentalSelector'] )
571 ) {
572 foreach ( $block_type->supports['__experimentalSelector'] as $key => $selector_metadata ) {
573 if ( ! isset( $selector_metadata['selector'] ) ) {
574 continue;
575 }
576
577 self::$blocks_metadata[ $key ] = array(
578 'selector' => $selector_metadata['selector'],
579 'supports' => $block_supports,
580 );
581 }
582 } else {
583 self::$blocks_metadata[ $block_name ] = array(
584 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ),
585 'supports' => $block_supports,
586 );
587 }
588 }
589
590 return self::$blocks_metadata;
591 }
592
593 /**
594 * Given a tree, removes the keys that are not present in the schema.
595 *
596 * It is recursive and modifies the input in-place.
597 *
598 * @param array $tree Input to process.
599 * @param array $schema Schema to adhere to.
600 *
601 * @return array Returns the modified $tree.
602 */
603 private static function remove_keys_not_in_schema( $tree, $schema ) {
604 $tree = array_intersect_key( $tree, $schema );
605
606 foreach ( $schema as $key => $data ) {
607 if ( is_array( $schema[ $key ] ) && isset( $tree[ $key ] ) ) {
608 $tree[ $key ] = self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
609
610 if ( empty( $tree[ $key ] ) ) {
611 unset( $tree[ $key ] );
612 }
613 }
614 }
615
616 return $tree;
617 }
618
619 /**
620 * Given a tree, it creates a flattened one
621 * by merging the keys and binding the leaf values
622 * to the new keys.
623 *
624 * It also transforms camelCase names into kebab-case
625 * and substitutes '/' by '-'.
626 *
627 * This is thought to be useful to generate
628 * CSS Custom Properties from a tree,
629 * although there's nothing in the implementation
630 * of this function that requires that format.
631 *
632 * For example, assuming the given prefix is '--wp'
633 * and the token is '--', for this input tree:
634 *
635 * {
636 * 'some/property': 'value',
637 * 'nestedProperty': {
638 * 'sub-property': 'value'
639 * }
640 * }
641 *
642 * it'll return this output:
643 *
644 * {
645 * '--wp--some-property': 'value',
646 * '--wp--nested-property--sub-property': 'value'
647 * }
648 *
649 * @param array $tree Input tree to process.
650 * @param string $prefix Prefix to prepend to each variable. '' by default.
651 * @param string $token Token to use between levels. '--' by default.
652 *
653 * @return array The flattened tree.
654 */
655 private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
656 $result = array();
657 foreach ( $tree as $property => $value ) {
658 $new_key = $prefix . str_replace(
659 '/',
660 '-',
661 strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
662 );
663
664 if ( is_array( $value ) ) {
665 $new_prefix = $new_key . $token;
666 $result = array_merge(
667 $result,
668 self::flatten_tree( $value, $new_prefix, $token )
669 );
670 } else {
671 $result[ $new_key ] = $value;
672 }
673 }
674 return $result;
675 }
676
677 /**
678 * Returns the style property for the given path.
679 *
680 * It also converts CSS Custom Property stored as
681 * "var:preset|color|secondary" to the form
682 * "--wp--preset--color--secondary".
683 *
684 * @param array $styles Styles subtree.
685 * @param array $path Which property to process.
686 *
687 * @return string Style property value.
688 */
689 private static function get_property_value( $styles, $path ) {
690 $value = gutenberg_experimental_get( $styles, $path, '' );
691
692 if ( '' === $value ) {
693 return $value;
694 }
695
696 $prefix = 'var:';
697 $prefix_len = strlen( $prefix );
698 $token_in = '|';
699 $token_out = '--';
700 if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
701 $unwrapped_name = str_replace(
702 $token_in,
703 $token_out,
704 substr( $value, $prefix_len )
705 );
706 $value = "var(--wp--$unwrapped_name)";
707 }
708
709 return $value;
710 }
711
712 /**
713 * Whether the metadata contains a key named properties.
714 *
715 * @param array $metadata Description of the style property.
716 *
717 * @return boolean True if properties exists, false otherwise.
718 */
719 private static function has_properties( $metadata ) {
720 if ( array_key_exists( 'properties', $metadata ) ) {
721 return true;
722 }
723
724 return false;
725 }
726
727 /**
728 * Given a styles array, it extracts the style properties
729 * and adds them to the $declarations array following the format:
730 *
731 * ```php
732 * array(
733 * 'name' => 'property_name',
734 * 'value' => 'property_value,
735 * )
736 * ```
737 *
738 * @param array $declarations Holds the existing declarations.
739 * @param array $styles Styles to process.
740 * @param array $supports Supports information for this block.
741 *
742 * @return array Returns the modified $declarations.
743 */
744 private static function compute_style_properties( $declarations, $styles, $supports ) {
745 if ( empty( $styles ) ) {
746 return $declarations;
747 }
748
749 $properties = array();
750 foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
751 if ( ! in_array( $name, $supports, true ) ) {
752 continue;
753 }
754
755 // Some properties can be shorthand properties, meaning that
756 // they contain multiple values instead of a single one.
757 // An example of this is the padding property, see self::SCHEMA.
758 if ( self::has_properties( $metadata ) ) {
759 foreach ( $metadata['properties'] as $property ) {
760 $properties[] = array(
761 'name' => $name . ucfirst( $property ),
762 'value' => array_merge( $metadata['value'], array( $property ) ),
763 );
764 }
765 } else {
766 $properties[] = array(
767 'name' => $name,
768 'value' => $metadata['value'],
769 );
770 }
771 }
772
773 foreach ( $properties as $prop ) {
774 $value = self::get_property_value( $styles, $prop['value'] );
775 if ( ! empty( $value ) ) {
776 $kebab_cased_name = self::to_kebab_case( $prop['name'] );
777 $declarations[] = array(
778 'name' => $kebab_cased_name,
779 'value' => $value,
780 );
781 }
782 }
783
784 return $declarations;
785 }
786
787 /**
788 * Given a settings array, it extracts its presets
789 * and adds them to the given input $stylesheet.
790 *
791 * @param string $stylesheet Input stylesheet to add the presets to.
792 * @param array $settings Settings to process.
793 * @param string $selector Selector wrapping the classes.
794 *
795 * @return the modified $stylesheet.
796 */
797 private static function compute_preset_classes( $stylesheet, $settings, $selector ) {
798 if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
799 // Classes at the global level do not need any CSS prefixed,
800 // and we don't want to increase its specificity.
801 $selector = '';
802 }
803
804 foreach ( self::PRESETS_METADATA as $preset ) {
805 $values = gutenberg_experimental_get( $settings, $preset['path'], array() );
806 foreach ( $values as $value ) {
807 foreach ( $preset['classes'] as $class ) {
808 $stylesheet .= self::to_ruleset(
809 $selector . '.has-' . $value['slug'] . '-' . $class['class_suffix'],
810 array(
811 array(
812 'name' => $class['property_name'],
813 'value' => $value[ $preset['value_key'] ],
814 ),
815 )
816 );
817 }
818 }
819 }
820
821 return $stylesheet;
822 }
823
824 /**
825 * Given the block settings, it extracts the CSS Custom Properties
826 * for the presets and adds them to the $declarations array
827 * following the format:
828 *
829 * ```php
830 * array(
831 * 'name' => 'property_name',
832 * 'value' => 'property_value,
833 * )
834 * ```
835 *
836 * @param array $declarations Holds the existing declarations.
837 * @param array $settings Settings to process.
838 *
839 * @return array Returns the modified $declarations.
840 */
841 private static function compute_preset_vars( $declarations, $settings ) {
842 foreach ( self::PRESETS_METADATA as $preset ) {
843 $values = gutenberg_experimental_get( $settings, $preset['path'], array() );
844 foreach ( $values as $value ) {
845 $declarations[] = array(
846 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'],
847 'value' => $value[ $preset['value_key'] ],
848 );
849 }
850 }
851
852 return $declarations;
853 }
854
855 /**
856 * Given an array of settings, it extracts the CSS Custom Properties
857 * for the custom values and adds them to the $declarations
858 * array following the format:
859 *
860 * ```php
861 * array(
862 * 'name' => 'property_name',
863 * 'value' => 'property_value,
864 * )
865 * ```
866 *
867 * @param array $declarations Holds the existing declarations.
868 * @param array $settings Settings to process.
869 *
870 * @return array Returns the modified $declarations.
871 */
872 private static function compute_theme_vars( $declarations, $settings ) {
873 $custom_values = gutenberg_experimental_get( $settings, array( 'custom' ) );
874 $css_vars = self::flatten_tree( $custom_values );
875 foreach ( $css_vars as $key => $value ) {
876 $declarations[] = array(
877 'name' => '--wp--custom--' . $key,
878 'value' => $value,
879 );
880 }
881
882 return $declarations;
883 }
884
885 /**
886 * Given a selector and a declaration list,
887 * creates the corresponding ruleset.
888 *
889 * To help debugging, will add some space
890 * if SCRIPT_DEBUG is defined and true.
891 *
892 * @param string $selector CSS selector.
893 * @param array $declarations List of declarations.
894 *
895 * @return string CSS ruleset.
896 */
897 private static function to_ruleset( $selector, $declarations ) {
898 if ( empty( $declarations ) ) {
899 return '';
900 }
901 $ruleset = '';
902
903 if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
904 $declaration_block = array_reduce(
905 $declarations,
906 function ( $carry, $element ) {
907 return $carry .= "\t" . $element['name'] . ': ' . $element['value'] . ";\n"; },
908 ''
909 );
910 $ruleset .= $selector . " {\n" . $declaration_block . "}\n";
911 } else {
912 $declaration_block = array_reduce(
913 $declarations,
914 function ( $carry, $element ) {
915 return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
916 ''
917 );
918 $ruleset .= $selector . '{' . $declaration_block . '}';
919 }
920
921 return $ruleset;
922 }
923
924 /**
925 * Converts each styles section into a list of rulesets
926 * to be appended to the stylesheet.
927 * These rulesets contain all the css variables (custom variables and preset variables).
928 *
929 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
930 *
931 * For each section this creates a new ruleset such as:
932 *
933 * block-selector {
934 * --wp--preset--category--slug: value;
935 * --wp--custom--variable: value;
936 * }
937 *
938 * @return string The new stylesheet.
939 */
940 private function get_css_variables() {
941 $stylesheet = '';
942 if ( ! isset( $this->theme_json['settings'] ) ) {
943 return $stylesheet;
944 }
945
946 $metadata = self::get_blocks_metadata();
947 foreach ( $this->theme_json['settings'] as $block_selector => $settings ) {
948 if ( empty( $metadata[ $block_selector ]['selector'] ) ) {
949 continue;
950 }
951 $selector = $metadata[ $block_selector ]['selector'];
952
953 $declarations = self::compute_preset_vars( array(), $settings );
954 $declarations = self::compute_theme_vars( $declarations, $settings );
955
956 // Attach the ruleset for style and custom properties.
957 $stylesheet .= self::to_ruleset( $selector, $declarations );
958 }
959 return $stylesheet;
960 }
961
962 /**
963 * Converts each style section into a list of rulesets
964 * containing the block styles to be appended to the stylesheet.
965 *
966 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
967 *
968 * For each section this creates a new ruleset such as:
969 *
970 * block-selector {
971 * style-property-one: value;
972 * }
973 *
974 * Additionally, it'll also create new rulesets
975 * as classes for each preset value such as:
976 *
977 * .has-value-color {
978 * color: value;
979 * }
980 *
981 * .has-value-background-color {
982 * background-color: value;
983 * }
984 *
985 * .has-value-font-size {
986 * font-size: value;
987 * }
988 *
989 * .has-value-gradient-background {
990 * background: value;
991 * }
992 *
993 * p.has-value-gradient-background {
994 * background: value;
995 * }
996 *
997 * @return string The new stylesheet.
998 */
999 private function get_block_styles() {
1000 $stylesheet = '';
1001 if ( ! isset( $this->theme_json['styles'] ) && ! isset( $this->theme_json['settings'] ) ) {
1002 return $stylesheet;
1003 }
1004
1005 $metadata = self::get_blocks_metadata();
1006 foreach ( $metadata as $block_selector => $metadata ) {
1007 if ( empty( $metadata['selector'] ) ) {
1008 continue;
1009 }
1010
1011 $selector = $metadata['selector'];
1012 $supports = $metadata['supports'];
1013
1014 $declarations = array();
1015 if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) {
1016 $declarations = self::compute_style_properties(
1017 $declarations,
1018 $this->theme_json['styles'][ $block_selector ],
1019 $supports
1020 );
1021 }
1022
1023 $stylesheet .= self::to_ruleset( $selector, $declarations );
1024
1025 // Attach the rulesets for the classes.
1026 if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) {
1027 $stylesheet = self::compute_preset_classes(
1028 $stylesheet,
1029 $this->theme_json['settings'][ $block_selector ],
1030 $selector
1031 );
1032 }
1033 }
1034
1035 return $stylesheet;
1036 }
1037
1038 /**
1039 * Returns the existing settings for each block.
1040 *
1041 * Example:
1042 *
1043 * {
1044 * 'root': {
1045 * 'color': {
1046 * 'custom': true
1047 * }
1048 * },
1049 * 'core/paragraph': {
1050 * 'spacing': {
1051 * 'customPadding': true
1052 * }
1053 * }
1054 * }
1055 *
1056 * @return array Settings per block.
1057 */
1058 public function get_settings() {
1059 if ( ! isset( $this->theme_json['settings'] ) ) {
1060 return array();
1061 } else {
1062 return $this->theme_json['settings'];
1063 }
1064 }
1065
1066 /**
1067 * Returns the page templates of the current theme.
1068 *
1069 * @return array
1070 */
1071 public function get_custom_templates() {
1072 if ( ! isset( $this->theme_json['customTemplates'] ) ) {
1073 return array();
1074 } else {
1075 return $this->theme_json['customTemplates'];
1076 }
1077 }
1078
1079 /**
1080 * Returns the template part data of current theme.
1081 *
1082 * @return array
1083 */
1084 public function get_template_parts() {
1085 if ( ! isset( $this->theme_json['templateParts'] ) ) {
1086 return array();
1087 }
1088 return $this->theme_json['templateParts'];
1089 }
1090
1091 /**
1092 * Returns the stylesheet that results of processing
1093 * the theme.json structure this object represents.
1094 *
1095 * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'.
1096 * @return string Stylesheet.
1097 */
1098 public function get_stylesheet( $type = 'all' ) {
1099 switch ( $type ) {
1100 case 'block_styles':
1101 return $this->get_block_styles();
1102 case 'css_variables':
1103 return $this->get_css_variables();
1104 default:
1105 return $this->get_css_variables() . $this->get_block_styles();
1106 }
1107 }
1108
1109 /**
1110 * Merge new incoming data.
1111 *
1112 * @param WP_Theme_JSON $incoming Data to merge.
1113 */
1114 public function merge( $incoming ) {
1115 $incoming_data = $incoming->get_raw_data();
1116 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
1117
1118 // The array_replace_recursive algorithm merges at the leaf level.
1119 // This means that when a leaf value is an array,
1120 // the incoming array won't replace the existing,
1121 // but the numeric indexes are used for replacement.
1122 //
1123 // These are the cases that have array values at the leaf levels.
1124 $block_metadata = self::get_blocks_metadata();
1125 foreach ( $block_metadata as $block_selector => $meta ) {
1126 // Color presets: palette & gradients.
1127 if ( isset( $incoming_data['settings'][ $block_selector ]['color']['palette'] ) ) {
1128 $this->theme_json['settings'][ $block_selector ]['color']['palette'] = $incoming_data['settings'][ $block_selector ]['color']['palette'];
1129 }
1130 if ( isset( $incoming_data['settings'][ $block_selector ]['color']['gradients'] ) ) {
1131 $this->theme_json['settings'][ $block_selector ]['color']['gradients'] = $incoming_data['settings'][ $block_selector ]['color']['gradients'];
1132 }
1133 // Spacing: units.
1134 if ( isset( $incoming_data['settings'][ $block_selector ]['spacing']['units'] ) ) {
1135 $this->theme_json['settings'][ $block_selector ]['spacing']['units'] = $incoming_data['settings'][ $block_selector ]['spacing']['units'];
1136 }
1137 // Typography presets: fontSizes & fontFamilies.
1138 if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontSizes'] ) ) {
1139 $this->theme_json['settings'][ $block_selector ]['typography']['fontSizes'] = $incoming_data['settings'][ $block_selector ]['typography']['fontSizes'];
1140 }
1141 if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies'] ) ) {
1142 $this->theme_json['settings'][ $block_selector ]['typography']['fontFamilies'] = $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies'];
1143 }
1144 // Custom section.
1145 if ( isset( $incoming_data['settings'][ $block_selector ]['custom'] ) ) {
1146 $this->theme_json['settings'][ $block_selector ]['custom'] = $incoming_data['settings'][ $block_selector ]['custom'];
1147 }
1148 }
1149 }
1150
1151 /**
1152 * Removes insecure data from theme.json.
1153 */
1154 public function remove_insecure_properties() {
1155 $blocks_metadata = self::get_blocks_metadata();
1156 foreach ( $blocks_metadata as $block_selector => $metadata ) {
1157 $escaped_settings = array();
1158 $escaped_styles = array();
1159
1160 // Style escaping.
1161 if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) {
1162 $declarations = self::compute_style_properties( array(), $this->theme_json['styles'][ $block_selector ], $metadata['supports'] );
1163 foreach ( $declarations as $declaration ) {
1164 $style_to_validate = $declaration['name'] . ': ' . $declaration['value'];
1165 if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) {
1166 $property = self::to_property( $declaration['name'] );
1167 $path = self::PROPERTIES_METADATA[ $property ]['value'];
1168 if ( self::has_properties( self::PROPERTIES_METADATA[ $property ] ) ) {
1169 $declaration_divided = explode( '-', $declaration['name'] );
1170 $path[] = $declaration_divided[1];
1171 }
1172 gutenberg_experimental_set(
1173 $escaped_styles,
1174 $path,
1175 gutenberg_experimental_get( $this->theme_json['styles'][ $block_selector ], $path )
1176 );
1177 }
1178 }
1179 }
1180
1181 // Settings escaping.
1182 // For now the ony allowed settings are presets.
1183 if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) {
1184 foreach ( self::PRESETS_METADATA as $preset_metadata ) {
1185 $current_preset = gutenberg_experimental_get(
1186 $this->theme_json['settings'][ $block_selector ],
1187 $preset_metadata['path'],
1188 null
1189 );
1190 if ( null !== $current_preset ) {
1191 $escaped_preset = array();
1192 foreach ( $current_preset as $single_preset ) {
1193 if (
1194 esc_attr( esc_html( $single_preset['name'] ) ) === $single_preset['name'] &&
1195 sanitize_html_class( $single_preset['slug'] ) === $single_preset['slug']
1196 ) {
1197 $value = $single_preset[ $preset_metadata['value_key'] ];
1198 $single_preset_is_valid = null;
1199 if ( isset( $preset_metadata['classes'] ) && count( $preset_metadata['classes'] ) > 0 ) {
1200 $single_preset_is_valid = true;
1201 foreach ( $preset_metadata['classes'] as $class_meta_data ) {
1202 $property = $class_meta_data['property_name'];
1203 $style_to_validate = $property . ': ' . $value;
1204 if ( esc_html( safecss_filter_attr( $style_to_validate ) ) !== $style_to_validate ) {
1205 $single_preset_is_valid = false;
1206 break;
1207 }
1208 }
1209 } else {
1210 $property = $preset_metadata['css_var_infix'];
1211 $style_to_validate = $property . ': ' . $value;
1212 $single_preset_is_valid = esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate;
1213 }
1214 if ( $single_preset_is_valid ) {
1215 $escaped_preset[] = $single_preset;
1216 }
1217 }
1218 }
1219 if ( ! empty( $escaped_preset ) ) {
1220 gutenberg_experimental_set( $escaped_settings, $preset_metadata['path'], $escaped_preset );
1221 }
1222 }
1223 }
1224 }
1225
1226 if ( empty( $escaped_settings ) ) {
1227 unset( $this->theme_json['settings'][ $block_selector ] );
1228 } else {
1229 $this->theme_json['settings'][ $block_selector ] = $escaped_settings;
1230 }
1231
1232 if ( empty( $escaped_styles ) ) {
1233 unset( $this->theme_json['styles'][ $block_selector ] );
1234 } else {
1235 $this->theme_json['styles'][ $block_selector ] = $escaped_styles;
1236 }
1237 }
1238 }
1239
1240 /**
1241 * Returns the raw data.
1242 *
1243 * @return array Raw data.
1244 */
1245 public function get_raw_data() {
1246 return $this->theme_json;
1247 }
1248
1249 }
1250