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

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