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

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

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