PluginProbe
Gutenberg / 10.0.0
Gutenberg v10.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 7.4.0 All 402 releases
gutenberg / lib / class-wp-theme-json.php

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

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