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