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

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