PluginProbe
Gutenberg / 10.4.1
Gutenberg v10.4.1
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / class-wp-theme-json.php

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

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