PluginProbe
Gutenberg / 9.9.0
Gutenberg v9.9.0
24.0.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 All 403 releases
gutenberg / lib / class-wp-theme-json.php

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

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