PluginProbe
Gutenberg / 23.1.1
Gutenberg v23.1.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-gutenberg.php

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

5,077 lines 177.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP_Theme_JSON_Gutenberg class
4 *
5 * @package gutenberg
6 * @since 5.8.0
7 */
8
9 /**
10 * Class that encapsulates the processing of structures that adhere to the theme.json spec.
11 *
12 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
13 * This is a low-level API that may need to do breaking changes. Please,
14 * use gutenberg_get_global_settings, gutenberg_get_global_styles, and gutenberg_get_global_stylesheet instead.
15 *
16 * @access private
17 */
18 #[AllowDynamicProperties]
19 class WP_Theme_JSON_Gutenberg {
20
21 /**
22 * Container of data in theme.json format.
23 *
24 * @since 5.8.0
25 * @var array
26 */
27 protected $theme_json = null;
28
29 /**
30 * Holds block metadata extracted from block.json
31 * to be shared among all instances so we don't
32 * process it twice.
33 *
34 * @since 5.8.0
35 * @since 6.1.0 Initialize as an empty array.
36 * @var array
37 */
38 protected static $blocks_metadata = array();
39
40 /**
41 * The CSS selector for the top-level preset settings.
42 *
43 * @since 6.6.0
44 * @var string
45 */
46 const ROOT_CSS_PROPERTIES_SELECTOR = ':root';
47
48 /**
49 * The CSS selector for the top-level styles.
50 *
51 * @since 5.8.0
52 * @var string
53 */
54 const ROOT_BLOCK_SELECTOR = 'body';
55
56 /**
57 * The sources of data this object can represent.
58 *
59 * @since 5.8.0
60 * @since 6.1.0 Added 'blocks'.
61 * @var string[]
62 */
63 const VALID_ORIGINS = array(
64 'default',
65 'blocks',
66 'theme',
67 'custom',
68 );
69
70 /**
71 * Presets are a set of values that serve
72 * to bootstrap some styles: colors, font sizes, etc.
73 *
74 * They are a unkeyed array of values such as:
75 *
76 * ```php
77 * array(
78 * array(
79 * 'slug' => 'unique-name-within-the-set',
80 * 'name' => 'Name for the UI',
81 * <value_key> => 'value'
82 * ),
83 * )
84 * ```
85 *
86 * This contains the necessary metadata to process them:
87 *
88 * - path => Where to find the preset within the settings section.
89 * - prevent_override => Disables override of default presets by theme presets.
90 * The relationship between whether to override the defaults
91 * and whether the defaults are enabled is inverse:
92 * - If defaults are enabled => theme presets should not be overridden
93 * - If defaults are disabled => theme presets should be overridden
94 * For example, a theme sets defaultPalette to false,
95 * making the default palette hidden from the user.
96 * In that case, we want all the theme presets to be present,
97 * so they should override the defaults by setting this false.
98 * - use_default_names => whether to use the default names
99 * - value_key => the key that represents the value
100 * - value_func => optionally, instead of value_key, a function to generate
101 * the value that takes a preset as an argument
102 * (either value_key or value_func should be present)
103 * - css_vars => template string to use in generating the CSS Custom Property.
104 * Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
105 * substituting the $slug for the slug's value for each preset value.
106 * - classes => array containing a structure with the classes to
107 * generate for the presets, where for each array item
108 * the key is the class name and the value the property name.
109 * The "$slug" substring will be replaced by the slug of each preset.
110 * For example:
111 * 'classes' => array(
112 * '.has-$slug-color' => 'color',
113 * '.has-$slug-background-color' => 'background-color',
114 * '.has-$slug-border-color' => 'border-color',
115 * )
116 * - properties => array of CSS properties to be used by kses to
117 * validate the content of each preset
118 * by means of the remove_insecure_properties method.
119 *
120 * @since 5.8.0
121 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
122 * `use_default_names` preset key, and simplified the metadata structure.
123 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
124 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
125 * @since 6.2.0 Added 'shadow' presets.
126 * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
127 * @since 6.6.0 Added `aspectRatios`.
128 * @var array
129 */
130 const PRESETS_METADATA = array(
131 array(
132 'path' => array( 'dimensions', 'aspectRatios' ),
133 'prevent_override' => array( 'dimensions', 'defaultAspectRatios' ),
134 'use_default_names' => false,
135 'value_key' => 'ratio',
136 'css_vars' => '--wp--preset--aspect-ratio--$slug',
137 'classes' => array(),
138 'properties' => array( 'aspect-ratio' ),
139 ),
140 array(
141 'path' => array( 'color', 'palette' ),
142 'prevent_override' => array( 'color', 'defaultPalette' ),
143 'use_default_names' => false,
144 'value_key' => 'color',
145 'css_vars' => '--wp--preset--color--$slug',
146 'classes' => array(
147 '.has-$slug-color' => 'color',
148 '.has-$slug-background-color' => 'background-color',
149 '.has-$slug-border-color' => 'border-color',
150 ),
151 'properties' => array( 'color', 'background-color', 'border-color' ),
152 ),
153 array(
154 'path' => array( 'color', 'gradients' ),
155 'prevent_override' => array( 'color', 'defaultGradients' ),
156 'use_default_names' => false,
157 'value_key' => 'gradient',
158 'css_vars' => '--wp--preset--gradient--$slug',
159 'classes' => array( '.has-$slug-gradient-background' => 'background' ),
160 'properties' => array( 'background' ),
161 ),
162 array(
163 'path' => array( 'color', 'duotone' ),
164 'prevent_override' => array( 'color', 'defaultDuotone' ),
165 'use_default_names' => false,
166 'value_func' => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone-gutenberg.php.
167 'css_vars' => null,
168 'classes' => array(),
169 'properties' => array( 'filter' ),
170 ),
171 array(
172 'path' => array( 'typography', 'fontSizes' ),
173 'prevent_override' => array( 'typography', 'defaultFontSizes' ),
174 'use_default_names' => true,
175 'value_func' => 'gutenberg_get_typography_font_size_value',
176 'css_vars' => '--wp--preset--font-size--$slug',
177 'classes' => array( '.has-$slug-font-size' => 'font-size' ),
178 'properties' => array( 'font-size' ),
179 ),
180 array(
181 'path' => array( 'typography', 'fontFamilies' ),
182 'prevent_override' => false,
183 'use_default_names' => false,
184 'value_key' => 'fontFamily',
185 'css_vars' => '--wp--preset--font-family--$slug',
186 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
187 'properties' => array( 'font-family' ),
188 ),
189 array(
190 'path' => array( 'spacing', 'spacingSizes' ),
191 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
192 'use_default_names' => true,
193 'value_key' => 'size',
194 'css_vars' => '--wp--preset--spacing--$slug',
195 'classes' => array(),
196 'properties' => array( 'padding', 'margin' ),
197 ),
198 array(
199 'path' => array( 'shadow', 'presets' ),
200 'prevent_override' => array( 'shadow', 'defaultPresets' ),
201 'use_default_names' => false,
202 'value_key' => 'shadow',
203 'css_vars' => '--wp--preset--shadow--$slug',
204 'classes' => array(),
205 'properties' => array( 'box-shadow' ),
206 ),
207 array(
208 'path' => array( 'border', 'radiusSizes' ),
209 'prevent_override' => false,
210 'use_default_names' => false,
211 'value_key' => 'size',
212 'css_vars' => '--wp--preset--border-radius--$slug',
213 'classes' => array(),
214 'properties' => array( 'border-radius' ),
215 ),
216 array(
217 'path' => array( 'dimensions', 'dimensionSizes' ),
218 'prevent_override' => false,
219 'use_default_names' => false,
220 'value_key' => 'size',
221 'css_vars' => '--wp--preset--dimension--$slug',
222 'classes' => array(),
223 'properties' => array( 'width', 'height', 'min-height' ),
224 ),
225 );
226
227 /**
228 * Metadata for style properties.
229 *
230 * Each element is a direct mapping from the CSS property name to the
231 * path to the value in theme.json & block attributes.
232 *
233 * @since 5.8.0
234 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
235 * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
236 * `text-decoration`, `text-transform`, and `filter` properties,
237 * simplified the metadata structure.
238 * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`,
239 * `--wp--style--root--padding-*`, and `box-shadow` properties,
240 * removed the `--wp--style--block-gap` property.
241 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
242 * @since 6.3.0 Added `writing-mode` property.
243 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
244 * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
245 * `typography.textIndent` properties.
246 *
247 * @var array
248 */
249 const PROPERTIES_METADATA = array(
250 'aspect-ratio' => array( 'dimensions', 'aspectRatio' ),
251 'background' => array( 'color', 'gradient' ),
252 'background-color' => array( 'color', 'background' ),
253 'background-image' => array( 'background', 'backgroundImage' ),
254 'background-position' => array( 'background', 'backgroundPosition' ),
255 'background-repeat' => array( 'background', 'backgroundRepeat' ),
256 'background-size' => array( 'background', 'backgroundSize' ),
257 'background-attachment' => array( 'background', 'backgroundAttachment' ),
258 'border-radius' => array( 'border', 'radius' ),
259 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ),
260 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ),
261 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ),
262 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
263 'border-color' => array( 'border', 'color' ),
264 'border-width' => array( 'border', 'width' ),
265 'border-style' => array( 'border', 'style' ),
266 'border-top-color' => array( 'border', 'top', 'color' ),
267 'border-top-width' => array( 'border', 'top', 'width' ),
268 'border-top-style' => array( 'border', 'top', 'style' ),
269 'border-right-color' => array( 'border', 'right', 'color' ),
270 'border-right-width' => array( 'border', 'right', 'width' ),
271 'border-right-style' => array( 'border', 'right', 'style' ),
272 'border-bottom-color' => array( 'border', 'bottom', 'color' ),
273 'border-bottom-width' => array( 'border', 'bottom', 'width' ),
274 'border-bottom-style' => array( 'border', 'bottom', 'style' ),
275 'border-left-color' => array( 'border', 'left', 'color' ),
276 'border-left-width' => array( 'border', 'left', 'width' ),
277 'border-left-style' => array( 'border', 'left', 'style' ),
278 'color' => array( 'color', 'text' ),
279 'text-align' => array( 'typography', 'textAlign' ),
280 'column-count' => array( 'typography', 'textColumns' ),
281 'font-family' => array( 'typography', 'fontFamily' ),
282 'font-size' => array( 'typography', 'fontSize' ),
283 'font-style' => array( 'typography', 'fontStyle' ),
284 'font-weight' => array( 'typography', 'fontWeight' ),
285 'letter-spacing' => array( 'typography', 'letterSpacing' ),
286 'line-height' => array( 'typography', 'lineHeight' ),
287 'margin' => array( 'spacing', 'margin' ),
288 'margin-top' => array( 'spacing', 'margin', 'top' ),
289 'margin-right' => array( 'spacing', 'margin', 'right' ),
290 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
291 'margin-left' => array( 'spacing', 'margin', 'left' ),
292 'min-height' => array( 'dimensions', 'minHeight' ),
293 'min-width' => array( 'dimensions', 'minWidth' ),
294 'outline-color' => array( 'outline', 'color' ),
295 'outline-offset' => array( 'outline', 'offset' ),
296 'outline-style' => array( 'outline', 'style' ),
297 'outline-width' => array( 'outline', 'width' ),
298 'padding' => array( 'spacing', 'padding' ),
299 'padding-top' => array( 'spacing', 'padding', 'top' ),
300 'padding-right' => array( 'spacing', 'padding', 'right' ),
301 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
302 'padding-left' => array( 'spacing', 'padding', 'left' ),
303 '--wp--style--root--padding' => array( 'spacing', 'padding' ),
304 '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ),
305 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
306 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
307 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
308 'text-decoration' => array( 'typography', 'textDecoration' ),
309 'text-transform' => array( 'typography', 'textTransform' ),
310 'text-indent' => array( 'typography', 'textIndent' ),
311 'filter' => array( 'filter', 'duotone' ),
312 'box-shadow' => array( 'shadow' ),
313 'height' => array( 'dimensions', 'height' ),
314 'width' => array( 'dimensions', 'width' ),
315 'writing-mode' => array( 'typography', 'writingMode' ),
316 );
317
318 /**
319 * Indirect metadata for style properties that are not directly output.
320 *
321 * Each element maps from a CSS property name to an array of
322 * paths to the value in theme.json & block attributes.
323 *
324 * Indirect properties are not output directly by `compute_style_properties`,
325 * but are used elsewhere in the processing of global styles. The indirect
326 * property is used to validate whether a style value is allowed.
327 *
328 * @since 6.2.0
329 * @since 6.6.0 Added background-image properties.
330 *
331 * @var array
332 */
333 const INDIRECT_PROPERTIES_METADATA = array(
334 'gap' => array(
335 array( 'spacing', 'blockGap' ),
336 ),
337 'column-gap' => array(
338 array( 'spacing', 'blockGap', 'left' ),
339 ),
340 'row-gap' => array(
341 array( 'spacing', 'blockGap', 'top' ),
342 ),
343 'max-width' => array(
344 array( 'layout', 'contentSize' ),
345 array( 'layout', 'wideSize' ),
346 ),
347 'background-image' => array(
348 array( 'background', 'backgroundImage', 'url' ),
349 array( 'background', 'gradient' ),
350 ),
351 );
352
353
354
355 /**
356 * The top-level keys a theme.json can have.
357 *
358 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
359 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
360 * added the `customTemplates` and `templateParts` values.
361 * @since 6.3.0 Added the `description` value.
362 * @var string[]
363 */
364 const VALID_TOP_LEVEL_KEYS = array(
365 'blockTypes',
366 'customTemplates',
367 'description',
368 'patterns',
369 'settings',
370 'styles',
371 'templateParts',
372 'title',
373 'slug',
374 'version',
375 );
376
377 /**
378 * The valid properties under the settings key.
379 *
380 * @since 5.8.0 As `ALLOWED_SETTINGS`.
381 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
382 * added new properties for `border`, `color`, `spacing`,
383 * and `typography`, and renamed others according to the new schema.
384 * @since 6.0.0 Added `color.defaultDuotone`.
385 * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
386 * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets',
387 * `position.fixed` and `position.sticky`.
388 * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
389 * @since 6.4.0 Added `layout.allowEditing`.
390 * @since 6.4.0 Added `lightbox`.
391 * @since 7.0.0 Added type markers to the schema for boolean values.
392 * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
393 * `typography.textIndent` properties.
394 * @var array
395 */
396 const VALID_SETTINGS = array(
397 'appearanceTools' => null,
398 'useRootPaddingAwareAlignments' => null,
399 'background' => array(
400 'backgroundImage' => null,
401 'backgroundSize' => null,
402 'gradient' => null,
403 ),
404 'border' => array(
405 'color' => null,
406 'radius' => null,
407 'style' => null,
408 'width' => null,
409 'radiusSizes' => null,
410 ),
411 'color' => array(
412 'background' => null,
413 'custom' => null,
414 'customDuotone' => null,
415 'customGradient' => null,
416 'defaultDuotone' => null,
417 'defaultGradients' => null,
418 'defaultPalette' => null,
419 'duotone' => null,
420 'gradients' => null,
421 'link' => null,
422 'heading' => null,
423 'button' => null,
424 'caption' => null,
425 'palette' => null,
426 'text' => null,
427 ),
428 'custom' => null,
429 'dimensions' => array(
430 'aspectRatio' => null,
431 'aspectRatios' => null,
432 'defaultAspectRatios' => null,
433 'dimensionSizes' => null,
434 'height' => null,
435 'minHeight' => null,
436 'minWidth' => null,
437 'width' => null,
438 ),
439 'layout' => array(
440 'contentSize' => null,
441 'wideSize' => null,
442 'allowEditing' => null,
443 'allowCustomContentAndWideSize' => null,
444 ),
445 'lightbox' => array(
446 'enabled' => true,
447 'allowEditing' => true,
448 ),
449 'position' => array(
450 'fixed' => null,
451 'sticky' => null,
452 ),
453 'spacing' => array(
454 'customSpacingSize' => null,
455 'defaultSpacingSizes' => null,
456 'spacingSizes' => null,
457 'spacingScale' => null,
458 'blockGap' => null,
459 'margin' => null,
460 'padding' => null,
461 'units' => null,
462 ),
463 'shadow' => array(
464 'presets' => null,
465 'defaultPresets' => null,
466 ),
467 'typography' => array(
468 'fluid' => null,
469 'customFontSize' => null,
470 'defaultFontSizes' => null,
471 'dropCap' => null,
472 'fontFamilies' => null,
473 'fontSizes' => null,
474 'fontStyle' => null,
475 'fontWeight' => null,
476 'letterSpacing' => null,
477 'lineHeight' => null,
478 'textAlign' => null,
479 'textColumns' => null,
480 'textDecoration' => null,
481 'textIndent' => null,
482 'textTransform' => null,
483 'writingMode' => null,
484 ),
485 );
486
487 const FONT_FAMILY_SCHEMA = array(
488 array(
489 'fontFamily' => null,
490 'name' => null,
491 'slug' => null,
492 'fontFace' => array(
493 array(
494 'ascentOverride' => null,
495 'descentOverride' => null,
496 'fontDisplay' => null,
497 'fontFamily' => null,
498 'fontFeatureSettings' => null,
499 'fontStyle' => null,
500 'fontStretch' => null,
501 'fontVariationSettings' => null,
502 'fontWeight' => null,
503 'lineGapOverride' => null,
504 'sizeAdjust' => null,
505 'src' => null,
506 'unicodeRange' => null,
507 ),
508 ),
509 ),
510 );
511
512 /**
513 * The valid properties under the styles key.
514 *
515 * @since 5.8.0 As `ALLOWED_STYLES`.
516 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
517 * added new properties for `border`, `filter`, `spacing`,
518 * and `typography`.
519 * @since 6.1.0 Added new side properties for `border`,
520 * added new property `shadow`,
521 * updated `blockGap` to be allowed at any level.
522 * @since 6.2.0 Added `outline`, and `minHeight` properties.
523 * @since 6.6.0 Added `background` sub properties to top-level only.
524 * @since 6.6.0 Added `dimensions.aspectRatio`.
525 * @since 7.0.0 Added `dimensions.width`, `dimensions.height`. and
526 * `typography.textIndent` properties.
527 * @var array
528 */
529 const VALID_STYLES = array(
530 'background' => array(
531 'backgroundImage' => null,
532 'backgroundAttachment' => null,
533 'backgroundPosition' => null,
534 'backgroundRepeat' => null,
535 'backgroundSize' => null,
536 'gradient' => null,
537 ),
538 'border' => array(
539 'color' => null,
540 'radius' => null,
541 'style' => null,
542 'width' => null,
543 'top' => null,
544 'right' => null,
545 'bottom' => null,
546 'left' => null,
547 ),
548 'color' => array(
549 'background' => null,
550 'gradient' => null,
551 'text' => null,
552 ),
553 'dimensions' => array(
554 'aspectRatio' => null,
555 'height' => null,
556 'minHeight' => null,
557 'minWidth' => null,
558 'width' => null,
559 ),
560 'filter' => array(
561 'duotone' => null,
562 ),
563 'outline' => array(
564 'color' => null,
565 'offset' => null,
566 'style' => null,
567 'width' => null,
568 ),
569 'shadow' => null,
570 'spacing' => array(
571 'margin' => null,
572 'padding' => null,
573 'blockGap' => null,
574 ),
575 'typography' => array(
576 'fontFamily' => null,
577 'fontSize' => null,
578 'fontStyle' => null,
579 'fontWeight' => null,
580 'letterSpacing' => null,
581 'lineHeight' => null,
582 'textAlign' => null,
583 'textColumns' => null,
584 'textDecoration' => null,
585 'textIndent' => null,
586 'textTransform' => null,
587 'writingMode' => null,
588 ),
589 'css' => null,
590 );
591
592 /**
593 * Defines which pseudo selectors are enabled for which elements.
594 *
595 * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active.
596 * This is to ensure the user action (hover, focus and active) styles have a higher
597 * specificity than the visited styles, which in turn have a higher specificity than
598 * the unvisited styles.
599 *
600 * See https://core.trac.wordpress.org/ticket/56928.
601 * Note: this will affect both top-level and block-level elements.
602 *
603 * @since 6.1.0
604 * @since 6.2.0 Added support for `:link` and `:any-link`.
605 * @since 6.8.0 Added support for `:focus-visible`.
606 */
607 const VALID_ELEMENT_PSEUDO_SELECTORS = array(
608 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
609 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
610 );
611
612 /**
613 * The valid pseudo-selectors that can be used for blocks.
614 *
615 * @since 7.0.0
616 * @var array
617 */
618 const VALID_BLOCK_PSEUDO_SELECTORS = array(
619 'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ),
620 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
621 );
622
623 /**
624 * Custom states for blocks that map to CSS class selectors rather than
625 * CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
626 * distinguish them from real CSS pseudo-selectors.
627 *
628 * The CSS selector for each state is defined in the block's block.json
629 * under `selectors.states`, e.g.:
630 *
631 * "selectors": { "states": { "@current": ".some-css-selector" } }
632 *
633 * This constant controls which states are valid in theme.json for a given
634 * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
635 * as valid sub-states, producing compound selectors such as
636 * `.wp-block-navigation-item.current-menu-item:hover`.
637 *
638 * @var array
639 */
640 const VALID_BLOCK_CUSTOM_STATES = array(
641 'core/navigation-link' => array( '@current' ),
642 );
643
644 /**
645 * The valid elements that can be found under styles.
646 *
647 * @since 5.8.0
648 * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
649 * @var string[]
650 */
651 const ELEMENTS = array(
652 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
653 'heading' => 'h1, h2, h3, h4, h5, h6',
654 'h1' => 'h1',
655 'h2' => 'h2',
656 'h3' => 'h3',
657 'h4' => 'h4',
658 'h5' => 'h5',
659 'h6' => 'h6',
660 // We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
661 'button' => '.wp-element-button, .wp-block-button__link',
662 // The block classes are necessary to target older content that won't use the new class names.
663 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
664 'cite' => 'cite',
665 'select' => 'select',
666 'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
667 );
668
669 const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
670 'button' => 'wp-element-button',
671 'caption' => 'wp-element-caption',
672 );
673
674 /**
675 * List of block support features that can have their related styles
676 * generated under their own feature level selector rather than the block's.
677 *
678 * @since 6.1.0
679 * @var string[]
680 */
681 const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
682 '__experimentalBorder' => 'border',
683 'color' => 'color',
684 'dimensions' => 'dimensions',
685 'spacing' => 'spacing',
686 'typography' => 'typography',
687 );
688
689 /**
690 * Return the input schema at the root and per origin.
691 *
692 * @since 6.5.0
693 *
694 * @param array $schema The base schema.
695 * @return array The schema at the root and per origin.
696 *
697 * Example:
698 * schema_in_root_and_per_origin(
699 * array(
700 * 'fontFamily' => null,
701 * 'slug' => null,
702 * )
703 * )
704 *
705 * Returns:
706 * array(
707 * 'fontFamily' => null,
708 * 'slug' => null,
709 * 'default' => array(
710 * 'fontFamily' => null,
711 * 'slug' => null,
712 * ),
713 * 'blocks' => array(
714 * 'fontFamily' => null,
715 * 'slug' => null,
716 * ),
717 * 'theme' => array(
718 * 'fontFamily' => null,
719 * 'slug' => null,
720 * ),
721 * 'custom' => array(
722 * 'fontFamily' => null,
723 * 'slug' => null,
724 * ),
725 * )
726 */
727 protected static function schema_in_root_and_per_origin( $schema ) {
728 $schema_in_root_and_per_origin = $schema;
729 foreach ( static::VALID_ORIGINS as $origin ) {
730 $schema_in_root_and_per_origin[ $origin ] = $schema;
731 }
732 return $schema_in_root_and_per_origin;
733 }
734
735 /**
736 * Processes pseudo-selectors for any node (block or variation).
737 *
738 * @param array $node The node data (block or variation).
739 * @param string $base_selector The base selector.
740 * @param array $settings The theme settings.
741 * @param string $block_name The block name.
742 * @return array Array of pseudo-selector declarations.
743 */
744 private static function process_pseudo_selectors( $node, $base_selector, $settings, $block_name ) {
745 $pseudo_declarations = array();
746
747 if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
748 return $pseudo_declarations;
749 }
750
751 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
752 if ( isset( $node[ $pseudo_selector ] ) ) {
753 $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
754 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null );
755 $pseudo_declarations[ $combined_selector ] = $declarations;
756 }
757 }
758
759 return $pseudo_declarations;
760 }
761
762 /**
763 * Returns a class name by an element name.
764 *
765 * @since 6.1.0
766 *
767 * @param string $element The name of the element.
768 * @return string The name of the class.
769 */
770 public static function get_element_class_name( $element ) {
771 $class_name = '';
772
773 if ( isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] ) ) {
774 $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
775 }
776
777 return $class_name;
778 }
779
780 /**
781 * Options that settings.appearanceTools enables.
782 *
783 * @since 6.0.0
784 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
785 * @since 7.0.0 Added `dimensions.width` and `dimensions.height`.
786 * @var array
787 */
788 const APPEARANCE_TOOLS_OPT_INS = array(
789 array( 'background', 'backgroundImage' ),
790 array( 'background', 'backgroundSize' ),
791 array( 'background', 'gradient' ),
792 array( 'border', 'color' ),
793 array( 'border', 'radius' ),
794 array( 'border', 'style' ),
795 array( 'border', 'width' ),
796 array( 'color', 'link' ),
797 array( 'color', 'heading' ),
798 array( 'color', 'button' ),
799 array( 'color', 'caption' ),
800 array( 'dimensions', 'aspectRatio' ),
801 array( 'dimensions', 'height' ),
802 array( 'dimensions', 'minHeight' ),
803 array( 'dimensions', 'minWidth' ),
804 array( 'dimensions', 'width' ),
805 // BEGIN EXPERIMENTAL.
806 // Allow `position.fixed` to be opted-in by default.
807 // Sticky position support was backported to WordPress 6.2 in https://core.trac.wordpress.org/ticket/57618.
808 // While `fixed` was included as a valid setting, exposing it by default is still experimental.
809 array( 'position', 'fixed' ),
810 // END EXPERIMENTAL.
811 array( 'position', 'sticky' ),
812 array( 'spacing', 'blockGap' ),
813 array( 'spacing', 'margin' ),
814 array( 'spacing', 'padding' ),
815 array( 'typography', 'lineHeight' ),
816 array( 'typography', 'textColumns' ),
817 );
818
819 /**
820 * The latest version of the schema in use.
821 *
822 * @since 5.8.0
823 * @since 5.9.0 Changed value from 1 to 2.
824 * @since 6.5.0 Changed value from 2 to 3.
825 * @var int
826 */
827 const LATEST_SCHEMA = 3;
828
829 /**
830 * Constructor.
831 *
832 * @since 5.8.0
833 * @since 6.6.0 Key spacingScale by origin, and pre-generate the spacingSizes from spacingScale.
834 * Added unwrapping of shared block style variations into block type variations if registered.
835 *
836 * @param array $theme_json A structure that follows the theme.json schema.
837 * @param string $origin Optional. What source of data this object represents.
838 * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
839 */
840 public function __construct( $theme_json = array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA ), $origin = 'theme' ) {
841 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
842 $origin = 'theme';
843 }
844
845 $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
846 $blocks_metadata = static::get_blocks_metadata();
847 $valid_block_names = array_keys( $blocks_metadata );
848 $valid_element_names = array_keys( static::ELEMENTS );
849 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
850 $this->theme_json = static::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations );
851 $this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
852 $this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json );
853
854 // Internally, presets are keyed by origin.
855 $nodes = static::get_setting_nodes( $this->theme_json );
856 foreach ( $nodes as $node ) {
857 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
858 $path = $node['path'];
859 foreach ( $preset_metadata['path'] as $subpath ) {
860 $path[] = $subpath;
861 }
862 $preset = _wp_array_get( $this->theme_json, $path, null );
863 if ( null !== $preset ) {
864 // If the preset is not already keyed by origin.
865 if ( isset( $preset[0] ) || empty( $preset ) ) {
866 _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
867 }
868 }
869 }
870 }
871
872 // In addition to presets, spacingScale (which generates presets) is also keyed by origin.
873 $scale_path = array( 'settings', 'spacing', 'spacingScale' );
874 $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
875 if ( null !== $spacing_scale ) {
876 // If the spacingScale is not already keyed by origin.
877 if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
878 _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
879 }
880 }
881
882 // Pre-generate the spacingSizes from spacingScale.
883 $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
884 $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
885 if ( isset( $spacing_scale ) ) {
886 $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
887 $spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() );
888 $spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale );
889 $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
890 _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
891 }
892 }
893
894 /**
895 * Unwraps shared block style variations.
896 *
897 * It takes the shared variations (styles.variations.variationName) and
898 * applies them to all the blocks that have the given variation registered
899 * (styles.blocks.blockType.variations.variationName).
900 *
901 * For example, given the `core/paragraph` and `core/group` blocks have
902 * registered the `section-a` style variation, and given the following input:
903 *
904 * {
905 * "styles": {
906 * "variations": {
907 * "section-a": { "color": { "background": "backgroundColor" } }
908 * }
909 * }
910 * }
911 *
912 * It returns the following output:
913 *
914 * {
915 * "styles": {
916 * "blocks": {
917 * "core/paragraph": {
918 * "variations": {
919 * "section-a": { "color": { "background": "backgroundColor" } }
920 * },
921 * },
922 * "core/group": {
923 * "variations": {
924 * "section-a": { "color": { "background": "backgroundColor" } }
925 * }
926 * }
927 * }
928 * }
929 * }
930 *
931 * @since 6.6.0
932 *
933 * @param array $theme_json A structure that follows the theme.json schema.
934 * @param array $valid_variations Valid block style variations.
935 *
936 * @return array Theme json data with shared variation definitions unwrapped under appropriate block types.
937 */
938 private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) {
939 if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) {
940 return $theme_json;
941 }
942
943 $new_theme_json = $theme_json;
944 $variations = $new_theme_json['styles']['variations'];
945
946 foreach ( $valid_variations as $block_type => $registered_variations ) {
947 foreach ( $registered_variations as $variation_name ) {
948 $block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
949 $top_level_data = $variations[ $variation_name ] ?? array();
950 $merged_data = array_replace_recursive( $top_level_data, $block_level_data );
951 if ( ! empty( $merged_data ) ) {
952 _wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data );
953 }
954 }
955 }
956
957 unset( $new_theme_json['styles']['variations'] );
958
959 return $new_theme_json;
960 }
961
962 /**
963 * Enables some opt-in settings if theme declared support.
964 *
965 * @since 5.9.0
966 *
967 * @param array $theme_json A theme.json structure to modify.
968 * @return array The modified theme.json structure.
969 */
970 protected static function maybe_opt_in_into_settings( $theme_json ) {
971 $new_theme_json = $theme_json;
972
973 if (
974 isset( $new_theme_json['settings']['appearanceTools'] ) &&
975 true === $new_theme_json['settings']['appearanceTools']
976 ) {
977 static::do_opt_in_into_settings( $new_theme_json['settings'] );
978 }
979
980 if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
981 foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
982 if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
983 static::do_opt_in_into_settings( $block );
984 }
985 }
986 }
987
988 return $new_theme_json;
989 }
990
991 /**
992 * Enables some settings.
993 *
994 * @since 5.9.0
995 *
996 * @param array $context The context to which the settings belong.
997 */
998 protected static function do_opt_in_into_settings( &$context ) {
999 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
1000 // Use "unset prop" as a marker instead of "null" because
1001 // "null" can be a valid value for some props (e.g. blockGap).
1002 if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
1003 _wp_array_set( $context, $path, true );
1004 }
1005 }
1006
1007 unset( $context['appearanceTools'] );
1008 }
1009
1010 /**
1011 * Sanitizes the input according to the schemas.
1012 *
1013 * @since 5.8.0
1014 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1015 * @since 6.6.0 Extended schema definition to allow enhanced block style variations.
1016 *
1017 * @param array $input Structure to sanitize.
1018 * @param array $valid_block_names List of valid block names.
1019 * @param array $valid_element_names List of valid element names.
1020 * @param array $valid_variations List of valid variations per block.
1021 * @return array The sanitized output.
1022 */
1023 protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) {
1024
1025 $output = array();
1026
1027 if ( ! is_array( $input ) ) {
1028 return $output;
1029 }
1030
1031 // Preserve only the top most level keys.
1032 $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
1033
1034 /*
1035 * Remove any rules that are annotated as "top" in VALID_STYLES constant.
1036 * Some styles are only meant to be available at the top-level (e.g.: blockGap),
1037 * hence, the schema for blocks & elements should not have them.
1038 */
1039 $styles_non_top_level = static::VALID_STYLES;
1040 foreach ( array_keys( $styles_non_top_level ) as $section ) {
1041 // array_key_exists() needs to be used instead of isset() because the value can be null.
1042 if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
1043 foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
1044 if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
1045 unset( $styles_non_top_level[ $section ][ $prop ] );
1046 }
1047 }
1048 }
1049 }
1050
1051 // Build the schema based on valid block & element names.
1052 $schema = array();
1053 $schema_styles_elements = array();
1054
1055 /*
1056 * Set allowed element pseudo selectors based on per element allow list.
1057 * Target data structure in schema:
1058 * e.g.
1059 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1060 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1061 */
1062 foreach ( $valid_element_names as $element ) {
1063 $schema_styles_elements[ $element ] = $styles_non_top_level;
1064
1065 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
1066 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
1067 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
1068 }
1069 }
1070 }
1071
1072 $schema_styles_blocks = array();
1073 $schema_settings_blocks = array();
1074
1075 /*
1076 * Generate a schema for blocks.
1077 * - Block styles can contain `elements` & `variations` definitions.
1078 * - Variations definitions cannot be nested.
1079 * - Variations can contain styles for inner `blocks`.
1080 * - Variation inner `blocks` styles can contain `elements`.
1081 *
1082 * As each variation needs a `blocks` schema but further nested
1083 * inner `blocks`, the overall schema will be generated in multiple passes.
1084 */
1085 foreach ( $valid_block_names as $block ) {
1086 $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
1087 $schema_styles_blocks[ $block ] = $styles_non_top_level;
1088 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
1089
1090 // Add pseudo-selectors for blocks that support them.
1091 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1092 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1093 $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1094 }
1095 }
1096
1097 // Add custom states for blocks that support them (e.g. '@current' for navigation).
1098 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1099 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1100 $custom_state_schema = $styles_non_top_level;
1101 // The same pseudo-selectors valid for the block at the top level
1102 // are also valid within each custom state.
1103 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1104 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo ) {
1105 $custom_state_schema[ $pseudo ] = $styles_non_top_level;
1106 }
1107 }
1108 $schema_styles_blocks[ $block ][ $custom_state ] = $custom_state_schema;
1109 }
1110 }
1111 }
1112
1113 $block_style_variation_styles = static::VALID_STYLES;
1114 $block_style_variation_styles['blocks'] = $schema_styles_blocks;
1115 $block_style_variation_styles['elements'] = $schema_styles_elements;
1116
1117 foreach ( $valid_block_names as $block ) {
1118 // Build the schema for each block style variation.
1119 $style_variation_names = array();
1120 if (
1121 ! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
1122 is_array( $input['styles']['blocks'][ $block ]['variations'] ) &&
1123 isset( $valid_variations[ $block ] )
1124 ) {
1125 $style_variation_names = array_intersect(
1126 array_keys( $input['styles']['blocks'][ $block ]['variations'] ),
1127 $valid_variations[ $block ]
1128 );
1129 }
1130
1131 $schema_styles_variations = array();
1132 if ( ! empty( $style_variation_names ) ) {
1133 foreach ( $style_variation_names as $variation_name ) {
1134 $variation_schema = $block_style_variation_styles;
1135
1136 // Add pseudo-selectors to variations for blocks that support them.
1137 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1138 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1139 $variation_schema[ $pseudo_selector ] = $styles_non_top_level;
1140 }
1141 }
1142
1143 $schema_styles_variations[ $variation_name ] = $variation_schema;
1144 }
1145 }
1146
1147 $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
1148 }
1149
1150 $schema['styles'] = static::VALID_STYLES;
1151 $schema['styles']['blocks'] = $schema_styles_blocks;
1152 $schema['styles']['elements'] = $schema_styles_elements;
1153 $schema['settings'] = static::VALID_SETTINGS;
1154 $schema['settings']['blocks'] = $schema_settings_blocks;
1155 $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1156
1157 // Remove anything that's not present in the schema.
1158 foreach ( array( 'styles', 'settings' ) as $subtree ) {
1159 if ( ! isset( $input[ $subtree ] ) ) {
1160 continue;
1161 }
1162
1163 if ( ! is_array( $input[ $subtree ] ) ) {
1164 unset( $output[ $subtree ] );
1165 continue;
1166 }
1167
1168 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1169
1170 if ( empty( $result ) ) {
1171 unset( $output[ $subtree ] );
1172 } else {
1173 $output[ $subtree ] = static::resolve_custom_css_format( $result );
1174 }
1175 }
1176
1177 return $output;
1178 }
1179
1180 /**
1181 * Appends a sub-selector to an existing one.
1182 *
1183 * Given the compounded $selector "h1, h2, h3"
1184 * and the $to_append selector ".some-class" the result will be
1185 * "h1.some-class, h2.some-class, h3.some-class".
1186 *
1187 * @since 5.8.0
1188 * @since 6.1.0 Added append position.
1189 * @since 6.3.0 Removed append position parameter.
1190 *
1191 * @param string $selector Original selector.
1192 * @param string $to_append Selector to append.
1193 * @return string The new selector.
1194 */
1195 protected static function append_to_selector( $selector, $to_append ) {
1196 if ( ! str_contains( $selector, ',' ) ) {
1197 return $selector . $to_append;
1198 }
1199 $new_selectors = array();
1200 $selectors = explode( ',', $selector );
1201 foreach ( $selectors as $sel ) {
1202 $new_selectors[] = $sel . $to_append;
1203 }
1204 return implode( ',', $new_selectors );
1205 }
1206
1207 /**
1208 * Prepends a sub-selector to an existing one.
1209 *
1210 * Given the compounded $selector "h1, h2, h3"
1211 * and the $to_prepend selector ".some-class " the result will be
1212 * ".some-class h1, .some-class h2, .some-class h3".
1213 *
1214 * @since 6.3.0
1215 *
1216 * @param string $selector Original selector.
1217 * @param string $to_prepend Selector to prepend.
1218 * @return string The new selector.
1219 */
1220 protected static function prepend_to_selector( $selector, $to_prepend ) {
1221 if ( ! str_contains( $selector, ',' ) ) {
1222 return $to_prepend . $selector;
1223 }
1224 $new_selectors = array();
1225 $selectors = explode( ',', $selector );
1226 foreach ( $selectors as $sel ) {
1227 $new_selectors[] = $to_prepend . $sel;
1228 }
1229 return implode( ',', $new_selectors );
1230 }
1231
1232 /**
1233 * Returns the metadata for each block.
1234 *
1235 * Example:
1236 *
1237 * {
1238 * 'core/paragraph': {
1239 * 'selector': 'p',
1240 * 'elements': {
1241 * 'link' => 'link selector',
1242 * 'etc' => 'element selector'
1243 * }
1244 * },
1245 * 'core/heading': {
1246 * 'selector': 'h1',
1247 * 'elements': {}
1248 * },
1249 * 'core/image': {
1250 * 'selector': '.wp-block-image',
1251 * 'duotone': 'img',
1252 * 'elements': {}
1253 * }
1254 * }
1255 *
1256 * @since 5.8.0
1257 * @since 5.9.0 Added `duotone` key with CSS selector.
1258 * @since 6.1.0 Added `features` key with block support feature level selectors.
1259 * @since 6.6.0 Added non-core block style variations to generated metadata.
1260 *
1261 * @return array Block metadata.
1262 */
1263 protected static function get_blocks_metadata() {
1264 // NOTE: the compat/6.1 version of this method in Gutenberg did not have these changes.
1265 $registry = WP_Block_Type_Registry::get_instance();
1266 $blocks = $registry->get_all_registered();
1267 $style_registry = WP_Block_Styles_Registry::get_instance();
1268
1269 // Is there metadata for all currently registered blocks?
1270 $blocks = array_diff_key( $blocks, static::$blocks_metadata );
1271 if ( empty( $blocks ) ) {
1272 /*
1273 * New block styles may have been registered within WP_Block_Styles_Registry.
1274 * Update block metadata for any new block style variations.
1275 */
1276 $registered_styles = $style_registry->get_all_registered();
1277 foreach ( static::$blocks_metadata as $block_name => $block_metadata ) {
1278 if ( ! empty( $registered_styles[ $block_name ] ) ) {
1279 $style_selectors = $block_metadata['styleVariations'] ?? array();
1280
1281 foreach ( $registered_styles[ $block_name ] as $block_style ) {
1282 if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) {
1283 $style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] );
1284 }
1285 }
1286
1287 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
1288 }
1289 }
1290 return static::$blocks_metadata;
1291 }
1292
1293 foreach ( $blocks as $block_name => $block_type ) {
1294 $root_selector = wp_get_block_css_selector( $block_type );
1295
1296 static::$blocks_metadata[ $block_name ]['selector'] = $root_selector;
1297 static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector );
1298
1299 $elements = static::get_block_element_selectors( $root_selector );
1300 if ( ! empty( $elements ) ) {
1301 static::$blocks_metadata[ $block_name ]['elements'] = $elements;
1302 }
1303
1304 // The block may or may not have a duotone selector.
1305 $duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' );
1306
1307 // Keep backwards compatibility for support.color.__experimentalDuotone.
1308 if ( null === $duotone_selector ) {
1309 $duotone_support = $block_type->supports['color']['__experimentalDuotone'] ?? null;
1310
1311 if ( $duotone_support ) {
1312 $root_selector = wp_get_block_css_selector( $block_type );
1313 $duotone_selector = static::scope_selector( $root_selector, $duotone_support );
1314 }
1315 }
1316
1317 if ( null !== $duotone_selector ) {
1318 static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
1319 }
1320
1321 // If the block has style variations, append their selectors to the block metadata.
1322 $style_selectors = array();
1323 if ( ! empty( $block_type->styles ) ) {
1324 foreach ( $block_type->styles as $style ) {
1325 $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
1326 }
1327 }
1328
1329 // Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json.
1330 $registered_styles = $style_registry->get_registered_styles_for_block( $block_name );
1331 foreach ( $registered_styles as $style ) {
1332 $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
1333 }
1334
1335 if ( ! empty( $style_selectors ) ) {
1336 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
1337 }
1338
1339 // If the block has custom states defined in block.json, store their selectors.
1340 if ( ! empty( $block_type->selectors['states'] ) && is_array( $block_type->selectors['states'] ) ) {
1341 static::$blocks_metadata[ $block_name ]['states'] = $block_type->selectors['states'];
1342 }
1343 }
1344
1345 return static::$blocks_metadata;
1346 }
1347
1348 /**
1349 * Given a tree, removes the keys that are not present in the schema.
1350 *
1351 * It is recursive and modifies the input in-place.
1352 *
1353 * @since 5.8.0
1354 * @since 7.0.0 Added type validation for boolean values.
1355 *
1356 * @param array $tree Input to process.
1357 * @param array $schema Schema to adhere to.
1358 * @return array The modified $tree.
1359 */
1360 protected static function remove_keys_not_in_schema( $tree, $schema ) {
1361 if ( ! is_array( $tree ) ) {
1362 return $tree;
1363 }
1364
1365 foreach ( $tree as $key => $value ) {
1366 // Remove keys not in the schema or with null/empty values.
1367 if ( ! array_key_exists( $key, $schema ) ) {
1368 unset( $tree[ $key ] );
1369 continue;
1370 }
1371
1372 // Validate type if schema specifies a boolean marker.
1373 if ( is_bool( $schema[ $key ] ) ) {
1374 // Schema expects a boolean value - validate the input matches.
1375 if ( ! is_bool( $value ) ) {
1376 unset( $tree[ $key ] );
1377 continue;
1378 }
1379 // Type matches, keep the value and continue to next key.
1380 continue;
1381 }
1382
1383 if ( is_array( $schema[ $key ] ) ) {
1384 if ( ! is_array( $value ) ) {
1385 unset( $tree[ $key ] );
1386 } elseif ( wp_is_numeric_array( $value ) ) {
1387 // If indexed, process each item in the array.
1388 foreach ( $value as $item_key => $item_value ) {
1389 if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
1390 $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
1391 } else {
1392 // If the schema does not define a further structure, keep the value as is.
1393 $tree[ $key ][ $item_key ] = $item_value;
1394 }
1395 }
1396 } else {
1397 // If associative, process as a single object.
1398 $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );
1399
1400 if ( empty( $tree[ $key ] ) ) {
1401 unset( $tree[ $key ] );
1402 }
1403 }
1404 }
1405 }
1406
1407 return $tree;
1408 }
1409
1410 /**
1411 * Returns the existing settings for each block.
1412 *
1413 * Example:
1414 *
1415 * {
1416 * 'root': {
1417 * 'color': {
1418 * 'custom': true
1419 * }
1420 * },
1421 * 'core/paragraph': {
1422 * 'spacing': {
1423 * 'customPadding': true
1424 * }
1425 * }
1426 * }
1427 *
1428 * @since 5.8.0
1429 *
1430 * @return array Settings per block.
1431 */
1432 public function get_settings() {
1433 if ( ! isset( $this->theme_json['settings'] ) ) {
1434 return array();
1435 } else {
1436 return $this->theme_json['settings'];
1437 }
1438 }
1439
1440 /**
1441 * Returns the stylesheet that results of processing
1442 * the theme.json structure this object represents.
1443 *
1444 * @since 5.8.0
1445 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
1446 * @since 6.6.0 Added option to skip root layout or block style variation styles.
1447 *
1448 * @param array $types Types of styles to load. Will load all by default. It accepts:
1449 * - `variables`: only the CSS Custom Properties for presets & custom ones.
1450 * - `styles`: only the styles section in theme.json.
1451 * - `presets`: only the classes for the presets.
1452 * - `custom-css`: only the custom CSS.
1453 * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
1454 * @param array $options An array of options for now used for internal purposes only (may change without notice).
1455 * The options currently supported are:
1456 * - 'scope' that makes sure all style are scoped to a given selector
1457 * - `root_selector` which overwrites and forces a given selector to be used on the root node
1458 * - `skip_root_layout_styles` which omits root layout styles from the generated stylesheet.
1459 * - `base_layout_styles` which when true generates only base layout styles without alignment rules. Defaults to false.
1460 * - `include_block_style_variations` which includes CSS for block style variations.
1461 * @return string The resulting stylesheet.
1462 */
1463 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
1464 if ( null === $origins ) {
1465 $origins = static::VALID_ORIGINS;
1466 }
1467
1468 if ( is_string( $types ) ) {
1469 // Dispatch error and map old arguments to new ones.
1470 _deprecated_argument( __FUNCTION__, '5.9.0' );
1471 if ( 'block_styles' === $types ) {
1472 $types = array( 'styles', 'presets' );
1473 } elseif ( 'css_variables' === $types ) {
1474 $types = array( 'variables' );
1475 } else {
1476 $types = array( 'variables', 'styles', 'presets' );
1477 }
1478 }
1479
1480 $blocks_metadata = static::get_blocks_metadata();
1481 $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options );
1482 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
1483
1484 $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
1485 $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
1486
1487 if ( ! empty( $options['scope'] ) ) {
1488 foreach ( $setting_nodes as &$node ) {
1489 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1490 }
1491 foreach ( $style_nodes as &$node ) {
1492 $node = static::scope_style_node_selectors( $options['scope'], $node );
1493 }
1494 unset( $node );
1495 }
1496
1497 if ( ! empty( $options['root_selector'] ) ) {
1498 if ( false !== $root_settings_key ) {
1499 $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
1500 }
1501 if ( false !== $root_style_key ) {
1502 $style_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
1503 }
1504 }
1505
1506 $stylesheet = '';
1507
1508 if ( in_array( 'variables', $types, true ) ) {
1509 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
1510 }
1511
1512 if ( in_array( 'styles', $types, true ) ) {
1513 if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) {
1514 $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ], $options );
1515 }
1516 $stylesheet .= $this->get_block_classes( $style_nodes );
1517 }
1518
1519 if ( in_array( 'presets', $types, true ) ) {
1520 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
1521 }
1522
1523 // Load the custom CSS last so it has the highest specificity.
1524 if ( in_array( 'custom-css', $types, true ) ) {
1525 // Add the global styles root CSS.
1526 $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
1527 }
1528
1529 return $stylesheet;
1530 }
1531
1532 /**
1533 * Processes the CSS, to apply nesting.
1534 *
1535 * @since 6.2.0
1536 *
1537 * @param string $css The CSS to process.
1538 * @param string $selector The selector to nest.
1539 * @return string The processed CSS.
1540 */
1541 public static function process_blocks_custom_css( $css, $selector ) {
1542 $processed_css = '';
1543
1544 if ( empty( $css ) ) {
1545 return $processed_css;
1546 }
1547
1548 // Split CSS nested rules.
1549 $parts = explode( '&', $css );
1550 foreach ( $parts as $part ) {
1551 if ( empty( $part ) ) {
1552 continue;
1553 }
1554 $is_root_css = ( ! str_contains( $part, '{' ) );
1555 if ( $is_root_css ) {
1556 // If the part doesn't contain braces, it applies to the root level.
1557 $processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}';
1558 } else {
1559 // If the part contains braces, it's a nested CSS rule.
1560 $part = explode( '{', str_replace( '}', '', $part ) );
1561 if ( count( $part ) !== 2 ) {
1562 continue;
1563 }
1564 $nested_selector = $part[0];
1565 $css_value = $part[1];
1566
1567 /*
1568 * Handle pseudo elements such as ::before, ::after etc. Regex will also
1569 * capture any leading combinator such as >, +, or ~, as well as spaces.
1570 * This allows pseudo elements as descendants e.g. `.parent ::before`.
1571 */
1572 $matches = array();
1573 $has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
1574 $pseudo_part = $has_pseudo_element ? $matches[1] : '';
1575 $nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;
1576
1577 // Finalize selector and re-append pseudo element if required.
1578 $part_selector = str_starts_with( $nested_selector, ' ' )
1579 ? static::scope_selector( $selector, $nested_selector )
1580 : static::append_to_selector( $selector, $nested_selector );
1581 $final_selector = ":root :where($part_selector)$pseudo_part";
1582
1583 $processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
1584 }
1585 }
1586 return $processed_css;
1587 }
1588
1589 /**
1590 * Returns the global styles custom css.
1591 *
1592 * @since 6.2.0
1593 * @deprecated 6.7.0 Use {@see 'get_stylesheet'} instead.
1594 *
1595 * @return string The global styles custom CSS.
1596 */
1597 public function get_custom_css() {
1598 _deprecated_function( __METHOD__, '6.7.0', 'get_stylesheet' );
1599 $block_custom_css = '';
1600 $block_nodes = $this->get_block_custom_css_nodes();
1601 foreach ( $block_nodes as $node ) {
1602 // The node selector will have its specificity set to 0-1-0 within process_blocks_custom_css.
1603 $block_custom_css .= $this->get_block_custom_css( $node['css'], $node['selector'] );
1604 }
1605
1606 return $this->get_base_custom_css() . $block_custom_css;
1607 }
1608
1609 /**
1610 * Returns the global styles base custom CSS.
1611 * This function is deprecated; please do not sync to core.
1612 *
1613 * @return string The global styles base custom CSS.
1614 */
1615 public function get_base_custom_css() {
1616 _deprecated_function( __METHOD__, 'Gutenberg 18.6.0', 'get_stylesheet' );
1617 return $this->theme_json['styles']['css'] ?? '';
1618 }
1619
1620 /**
1621 * Returns the block nodes with custom CSS.
1622 * This function is deprecated; please do not sync to core.
1623 *
1624 * @return array The block nodes.
1625 */
1626 public function get_block_custom_css_nodes() {
1627 _deprecated_function( __METHOD__, 'Gutenberg 18.6.0', 'get_block_nodes' );
1628 $block_nodes = array();
1629
1630 // Add the global styles block CSS.
1631 if ( isset( $this->theme_json['styles']['blocks'] ) ) {
1632 foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
1633 $custom_block_css = $this->theme_json['styles']['blocks'][ $name ]['css'] ?? null;
1634 if ( $custom_block_css ) {
1635 $block_nodes[] = array(
1636 'name' => $name,
1637 'selector' => static::$blocks_metadata[ $name ]['selector'],
1638 'css' => $custom_block_css,
1639 );
1640 }
1641 }
1642 }
1643
1644 return $block_nodes;
1645 }
1646
1647 /**
1648 * Returns the global styles custom CSS for a single block.
1649 * This function is deprecated; please do not sync to core.
1650 *
1651 * @param array $css The block css node.
1652 * @param string $selector The block selector.
1653 *
1654 * @return string The global styles custom CSS for the block.
1655 */
1656 public function get_block_custom_css( $css, $selector ) {
1657 _deprecated_function( __METHOD__, 'Gutenberg 18.6.0', 'get_styles_for_block' );
1658 return $this->process_blocks_custom_css( $css, $selector );
1659 }
1660
1661 /**
1662 * Returns the page templates of the active theme.
1663 *
1664 * @since 5.9.0
1665 *
1666 * @return array
1667 */
1668 public function get_custom_templates() {
1669 $custom_templates = array();
1670 if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
1671 return $custom_templates;
1672 }
1673
1674 foreach ( $this->theme_json['customTemplates'] as $item ) {
1675 if ( isset( $item['name'] ) ) {
1676 $custom_templates[ $item['name'] ] = array(
1677 'title' => $item['title'] ?? '',
1678 'postTypes' => $item['postTypes'] ?? array( 'page' ),
1679 );
1680 }
1681 }
1682 return $custom_templates;
1683 }
1684
1685 /**
1686 * Returns the template part data of active theme.
1687 *
1688 * @since 5.9.0
1689 *
1690 * @return array
1691 */
1692 public function get_template_parts() {
1693 $template_parts = array();
1694 if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
1695 return $template_parts;
1696 }
1697
1698 foreach ( $this->theme_json['templateParts'] as $item ) {
1699 if ( isset( $item['name'] ) ) {
1700 $template_parts[ $item['name'] ] = array(
1701 'title' => $item['title'] ?? '',
1702 'area' => $item['area'] ?? '',
1703 );
1704 }
1705 }
1706 return $template_parts;
1707 }
1708
1709 /**
1710 * Converts each style section into a list of rulesets
1711 * containing the block styles to be appended to the stylesheet.
1712 *
1713 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
1714 *
1715 * For each section this creates a new ruleset such as:
1716 *
1717 * block-selector {
1718 * style-property-one: value;
1719 * }
1720 *
1721 * @since 5.8.0 As `get_block_styles()`.
1722 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
1723 * and no longer returns preset classes.
1724 * Removed the `$setting_nodes` parameter.
1725 * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`.
1726 *
1727 * @param array $style_nodes Nodes with styles.
1728 * @return string The new stylesheet.
1729 */
1730 protected function get_block_classes( $style_nodes ) {
1731 $block_rules = '';
1732
1733 foreach ( $style_nodes as $metadata ) {
1734 if ( null === $metadata['selector'] ) {
1735 continue;
1736 }
1737 $block_rules .= static::get_styles_for_block( $metadata );
1738 }
1739
1740 return $block_rules;
1741 }
1742
1743 /**
1744 * Gets the CSS layout rules for a particular block from theme.json layout definitions.
1745 *
1746 * @since 6.1.0
1747 *
1748 * @param array $block_metadata Metadata about the block to get styles for.
1749 * @param array $options Optional. An array of options for now used for internal purposes only.
1750 * @return string Layout styles for the block.
1751 */
1752 protected function get_layout_styles( $block_metadata, $options = array() ) {
1753 $block_rules = '';
1754 $block_type = null;
1755
1756 // Skip outputting layout styles if explicitly disabled.
1757 if ( current_theme_supports( 'disable-layout-styles' ) ) {
1758 return $block_rules;
1759 }
1760
1761 if ( isset( $block_metadata['name'] ) ) {
1762 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
1763 if ( ! block_has_support( $block_type, array( 'layout' ), false ) && ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) {
1764 return $block_rules;
1765 }
1766 }
1767
1768 $selector = $block_metadata['selector'] ?? '';
1769 $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] );
1770 $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support.
1771 $node = $options['node'] ?? _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
1772 $layout_definitions = gutenberg_get_layout_definitions();
1773 $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\,\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
1774
1775 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1776 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1777 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1778 $block_gap_value = null;
1779 // Use a fallback gap value if block gap support is not available.
1780 if ( ! $has_block_gap_support ) {
1781 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1782 if ( ! empty( $block_type ) ) {
1783 $block_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null;
1784 }
1785 } else {
1786 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1787 }
1788
1789 // Support split row / column values and concatenate to a shorthand value.
1790 if ( is_array( $block_gap_value ) ) {
1791 if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1792 $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1793 $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1794 $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
1795 } else {
1796 // Skip outputting gap value if not all sides are provided.
1797 $block_gap_value = null;
1798 }
1799 }
1800
1801 // If the block should have custom gap, add the gap styles.
1802 if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) {
1803 foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
1804 // Allow outputting fallback gap styles for flex layout type when block gap support isn't available.
1805 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
1806 continue;
1807 }
1808
1809 $class_name = $layout_definition['className'] ?? false;
1810 $spacing_rules = $layout_definition['spacingStyles'] ?? array();
1811
1812 if (
1813 ! empty( $class_name ) &&
1814 ! empty( $spacing_rules )
1815 ) {
1816 foreach ( $spacing_rules as $spacing_rule ) {
1817 $declarations = array();
1818 if (
1819 isset( $spacing_rule['selector'] ) &&
1820 preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
1821 ! empty( $spacing_rule['rules'] )
1822 ) {
1823 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1824 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1825 $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
1826 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1827 $declarations[] = array(
1828 'name' => $css_property,
1829 'value' => $current_css_value,
1830 );
1831 }
1832 }
1833
1834 if ( ! $has_block_gap_support ) {
1835 // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
1836 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)';
1837 $layout_selector = sprintf(
1838 $format,
1839 $selector,
1840 $class_name,
1841 $spacing_rule['selector']
1842 );
1843 } else {
1844 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s';
1845 $layout_selector = sprintf(
1846 $format,
1847 $selector,
1848 $class_name,
1849 $spacing_rule['selector']
1850 );
1851 }
1852 $block_rules .= static::to_ruleset( $layout_selector, $declarations );
1853 }
1854 }
1855 }
1856 }
1857 }
1858 }
1859
1860 // Output base styles.
1861 if (
1862 static::ROOT_BLOCK_SELECTOR === $selector
1863 ) {
1864 $valid_display_modes = array( 'block', 'flex', 'grid' );
1865 foreach ( $layout_definitions as $layout_definition ) {
1866 $class_name = $layout_definition['className'] ?? false;
1867 $base_style_rules = $layout_definition['baseStyles'] ?? array();
1868
1869 if (
1870 ! empty( $class_name ) &&
1871 is_array( $base_style_rules )
1872 ) {
1873 // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
1874 if (
1875 ! empty( $layout_definition['displayMode'] ) &&
1876 is_string( $layout_definition['displayMode'] ) &&
1877 in_array( $layout_definition['displayMode'], $valid_display_modes, true )
1878 ) {
1879 $layout_selector = sprintf(
1880 '%s .%s',
1881 $selector,
1882 $class_name
1883 );
1884 $block_rules .= static::to_ruleset(
1885 $layout_selector,
1886 array(
1887 array(
1888 'name' => 'display',
1889 'value' => $layout_definition['displayMode'],
1890 ),
1891 )
1892 );
1893 }
1894
1895 foreach ( $base_style_rules as $base_style_rule ) {
1896 $declarations = array();
1897
1898 // Skip outputting base styles for flow and constrained layout types when base_layout_styles is enabled.
1899 // These themes don't use .wp-site-blocks wrapper, so these layout-specific alignment styles aren't needed.
1900 if ( ! empty( $options['base_layout_styles'] ) && ( 'default' === $layout_definition['name'] || 'constrained' === $layout_definition['name'] ) ) {
1901 continue;
1902 }
1903
1904 if (
1905 isset( $base_style_rule['selector'] ) &&
1906 preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
1907 ! empty( $base_style_rule['rules'] )
1908 ) {
1909 foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
1910 // Skip rules that reference content size or wide size if they are not defined in the theme.json.
1911 if (
1912 is_string( $css_value ) &&
1913 ( str_contains( $css_value, '--global--content-size' ) || str_contains( $css_value, '--global--wide-size' ) ) &&
1914 ! isset( $this->theme_json['settings']['layout']['contentSize'] ) &&
1915 ! isset( $this->theme_json['settings']['layout']['wideSize'] )
1916 ) {
1917 continue;
1918 }
1919
1920 if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
1921 $declarations[] = array(
1922 'name' => $css_property,
1923 'value' => $css_value,
1924 );
1925 }
1926 }
1927
1928 $layout_selector = sprintf(
1929 '.%s%s',
1930 $class_name,
1931 $base_style_rule['selector']
1932 );
1933 $block_rules .= static::to_ruleset( $layout_selector, $declarations );
1934 }
1935 }
1936 }
1937 }
1938 }
1939 return $block_rules;
1940 }
1941
1942 /**
1943 * Creates new rulesets as classes for each preset value such as:
1944 *
1945 * .has-value-color {
1946 * color: value;
1947 * }
1948 *
1949 * .has-value-background-color {
1950 * background-color: value;
1951 * }
1952 *
1953 * .has-value-font-size {
1954 * font-size: value;
1955 * }
1956 *
1957 * .has-value-gradient-background {
1958 * background: value;
1959 * }
1960 *
1961 * p.has-value-gradient-background {
1962 * background: value;
1963 * }
1964 *
1965 * @since 5.9.0
1966 *
1967 * @param array $setting_nodes Nodes with settings.
1968 * @param array $origins List of origins to process presets from.
1969 * @return string The new stylesheet.
1970 */
1971 protected function get_preset_classes( $setting_nodes, $origins ) {
1972 $preset_rules = '';
1973
1974 foreach ( $setting_nodes as $metadata ) {
1975 if ( null === $metadata['selector'] ) {
1976 continue;
1977 }
1978
1979 $selector = $metadata['selector'];
1980 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1981 $preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
1982 }
1983
1984 return $preset_rules;
1985 }
1986
1987 /**
1988 * Converts each styles section into a list of rulesets
1989 * to be appended to the stylesheet.
1990 * These rulesets contain all the css variables (custom variables and preset variables).
1991 *
1992 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
1993 *
1994 * For each section this creates a new ruleset such as:
1995 *
1996 * block-selector {
1997 * --wp--preset--category--slug: value;
1998 * --wp--custom--variable: value;
1999 * }
2000 *
2001 * @since 5.8.0
2002 * @since 5.9.0 Added the `$origins` parameter.
2003 *
2004 * @param array $nodes Nodes with settings.
2005 * @param array $origins List of origins to process.
2006 * @return string The new stylesheet.
2007 */
2008 protected function get_css_variables( $nodes, $origins ) {
2009 $stylesheet = '';
2010 foreach ( $nodes as $metadata ) {
2011 if ( null === $metadata['selector'] ) {
2012 continue;
2013 }
2014
2015 $selector = $metadata['selector'];
2016 $feature_selectors = $metadata['selectors'] ?? array();
2017 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
2018
2019 /*
2020 * Group preset declarations by selector. Blocks that define
2021 * feature-level selectors need their preset CSS variables
2022 * output under that feature selector instead of the block's
2023 * root selector.
2024 */
2025 $vars_by_selector = array();
2026 $vars_by_selector[ $selector ] = array();
2027
2028 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2029 if ( empty( $preset_metadata['css_vars'] ) ) {
2030 continue;
2031 }
2032
2033 $values_by_slug = static::get_settings_values_by_slug( $node, $preset_metadata, $origins );
2034 if ( empty( $values_by_slug ) ) {
2035 continue;
2036 }
2037
2038 $target = static::get_feature_selector( $feature_selectors, $preset_metadata['path'][0], $selector );
2039
2040 if ( ! isset( $vars_by_selector[ $target ] ) ) {
2041 $vars_by_selector[ $target ] = array();
2042 }
2043
2044 foreach ( $values_by_slug as $slug => $value ) {
2045 $vars_by_selector[ $target ][] = array(
2046 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
2047 'value' => $value,
2048 );
2049 }
2050 }
2051
2052 // Theme vars always use the block's default selector.
2053 foreach ( static::compute_theme_vars( $node ) as $theme_var ) {
2054 $vars_by_selector[ $selector ][] = $theme_var;
2055 }
2056
2057 foreach ( $vars_by_selector as $rule_selector => $declarations ) {
2058 $stylesheet .= static::to_ruleset( $rule_selector, $declarations );
2059 }
2060 }
2061
2062 return $stylesheet;
2063 }
2064
2065 /**
2066 * Returns the appropriate selector for a block support feature's
2067 * preset CSS variables.
2068 *
2069 * If the block defines a feature-level selector (as a string or an
2070 * object with a `root` key), that selector is returned. Otherwise,
2071 * the block's default selector is used.
2072 *
2073 * @param array<string, string|array<string, string>> $feature_selectors The block's feature selectors map.
2074 * @param string $feature_key The feature to look up (e.g. 'dimensions').
2075 * @param string $default_selector Fallback selector.
2076 * @return string The resolved selector.
2077 */
2078 private static function get_feature_selector( array $feature_selectors, string $feature_key, string $default_selector ): string {
2079 if ( ! isset( $feature_selectors[ $feature_key ] ) ) {
2080 return $default_selector;
2081 }
2082
2083 $feature = $feature_selectors[ $feature_key ];
2084
2085 if ( is_string( $feature ) ) {
2086 return $feature;
2087 }
2088
2089 if ( isset( $feature['root'] ) && is_string( $feature['root'] ) ) {
2090 return $feature['root'];
2091 }
2092
2093 return $default_selector;
2094 }
2095
2096 /**
2097 * Given a selector and a declaration list,
2098 * creates the corresponding ruleset.
2099 *
2100 * @since 5.8.0
2101 * @since 7.1.0 Skip declarations whose value is not a plain string (booleans, arrays, objects, etc.).
2102 *
2103 * @param string $selector CSS selector.
2104 * @param array $declarations List of declarations.
2105 * @return string The resulting CSS ruleset.
2106 */
2107 protected static function to_ruleset( $selector, $declarations ) {
2108 if ( empty( $declarations ) ) {
2109 return '';
2110 }
2111
2112 $declaration_block = array_reduce(
2113 $declarations,
2114 static function ( $carry, $element ) {
2115 $value = $element['value'];
2116
2117 if ( is_numeric( $value ) ) {
2118 $value = (string) $value;
2119 }
2120
2121 if ( ! is_string( $value ) ) {
2122 return $carry;
2123 }
2124
2125 return $carry .= $element['name'] . ': ' . $value . ';';
2126 },
2127 ''
2128 );
2129
2130 return $selector . '{' . $declaration_block . '}';
2131 }
2132
2133 /**
2134 * Given a settings array, returns the generated rulesets
2135 * for the preset classes.
2136 *
2137 * @since 5.8.0
2138 * @since 5.9.0 Added the `$origins` parameter.
2139 *
2140 * @param array $settings Settings to process.
2141 * @param string $selector Selector wrapping the classes.
2142 * @param array $origins List of origins to process.
2143 * @return string The result of processing the presets.
2144 */
2145 protected static function compute_preset_classes( $settings, $selector, $origins ) {
2146 if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) {
2147 // Classes at the global level do not need any CSS prefixed,
2148 // and we don't want to increase its specificity.
2149 $selector = '';
2150 }
2151
2152 $stylesheet = '';
2153 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2154 if ( empty( $preset_metadata['classes'] ) ) {
2155 continue;
2156 }
2157
2158 $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
2159 foreach ( $preset_metadata['classes'] as $class => $property ) {
2160 foreach ( $slugs as $slug ) {
2161 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2162 $class_name = static::replace_slug_in_string( $class, $slug );
2163
2164 // $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
2165 $new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name );
2166 $stylesheet .= static::to_ruleset(
2167 $new_selector,
2168 array(
2169 array(
2170 'name' => $property,
2171 'value' => 'var(' . $css_var . ') !important',
2172 ),
2173 )
2174 );
2175 }
2176 }
2177 }
2178
2179 return $stylesheet;
2180 }
2181
2182 /**
2183 * Function that scopes a selector with another one. This works a bit like
2184 * SCSS nesting except the `&` operator isn't supported.
2185 *
2186 * <code>
2187 * $scope = '.a, .b .c';
2188 * $selector = '> .x, .y';
2189 * $merged = scope_selector( $scope, $selector );
2190 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
2191 * </code>
2192 *
2193 * @since 5.9.0
2194 *
2195 * @param string $scope Selector to scope to.
2196 * @param string $selector Original selector.
2197 * @return string Scoped selector.
2198 */
2199 public static function scope_selector( $scope, $selector ) {
2200 if ( ! $scope || ! $selector ) {
2201 return $selector;
2202 }
2203
2204 $scopes = explode( ',', $scope );
2205 $selectors = explode( ',', $selector );
2206
2207 $selectors_scoped = array();
2208 foreach ( $scopes as $outer ) {
2209 foreach ( $selectors as $inner ) {
2210 $outer = trim( $outer );
2211 $inner = trim( $inner );
2212 if ( ! empty( $outer ) && ! empty( $inner ) ) {
2213 $selectors_scoped[] = $outer . ' ' . $inner;
2214 } elseif ( empty( $outer ) ) {
2215 $selectors_scoped[] = $inner;
2216 } elseif ( empty( $inner ) ) {
2217 $selectors_scoped[] = $outer;
2218 }
2219 }
2220 }
2221
2222 $result = implode( ', ', $selectors_scoped );
2223 return $result;
2224 }
2225
2226 /**
2227 * Scopes the selectors for a given style node. This includes the primary
2228 * selector, i.e. `$node['selector']`, as well as any custom selectors for
2229 * features and subfeatures, e.g. `$node['selectors']['border']` etc.
2230 *
2231 * @since 6.6.0
2232 *
2233 * @param string $scope Selector to scope to.
2234 * @param array $node Style node with selectors to scope.
2235 *
2236 * @return array Node with updated selectors.
2237 */
2238 protected static function scope_style_node_selectors( $scope, $node ) {
2239 $node['selector'] = static::scope_selector( $scope, $node['selector'] );
2240
2241 if ( empty( $node['selectors'] ) ) {
2242 return $node;
2243 }
2244
2245 foreach ( $node['selectors'] as $feature => $selector ) {
2246 if ( is_string( $selector ) ) {
2247 $node['selectors'][ $feature ] = static::scope_selector( $scope, $selector );
2248 }
2249 if ( is_array( $selector ) ) {
2250 foreach ( $selector as $subfeature => $subfeature_selector ) {
2251 $node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector );
2252 }
2253 }
2254 }
2255
2256 return $node;
2257 }
2258
2259 /**
2260 * Gets preset values keyed by slugs based on settings and metadata.
2261 *
2262 * <code>
2263 * $settings = array(
2264 * 'typography' => array(
2265 * 'fontFamilies' => array(
2266 * array(
2267 * 'slug' => 'sansSerif',
2268 * 'fontFamily' => '"Helvetica Neue", sans-serif',
2269 * ),
2270 * array(
2271 * 'slug' => 'serif',
2272 * 'colors' => 'Georgia, serif',
2273 * )
2274 * ),
2275 * ),
2276 * );
2277 * $meta = array(
2278 * 'path' => array( 'typography', 'fontFamilies' ),
2279 * 'value_key' => 'fontFamily',
2280 * );
2281 * $values_by_slug = get_settings_values_by_slug();
2282 * // $values_by_slug === array(
2283 * // 'sans-serif' => '"Helvetica Neue", sans-serif',
2284 * // 'serif' => 'Georgia, serif',
2285 * // );
2286 * </code>
2287 *
2288 * @since 5.9.0
2289 * @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA.
2290 *
2291 * @param array $settings Settings to process.
2292 * @param array $preset_metadata One of the PRESETS_METADATA values.
2293 * @param array $origins List of origins to process.
2294 * @return array Array of presets where each key is a slug and each value is the preset value.
2295 */
2296 protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
2297 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
2298
2299 $result = array();
2300 foreach ( $origins as $origin ) {
2301 if ( ! isset( $preset_per_origin[ $origin ] ) ) {
2302 continue;
2303 }
2304 foreach ( $preset_per_origin[ $origin ] as $preset ) {
2305 $slug = _wp_to_kebab_case( $preset['slug'] );
2306
2307 $value = '';
2308 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
2309 $value_key = $preset_metadata['value_key'];
2310 $value = $preset[ $value_key ];
2311 } elseif (
2312 isset( $preset_metadata['value_func'] ) &&
2313 is_callable( $preset_metadata['value_func'] )
2314 ) {
2315 $value_func = $preset_metadata['value_func'];
2316 $value = call_user_func( $value_func, $preset, $settings );
2317 } else {
2318 // If we don't have a value, then don't add it to the result.
2319 continue;
2320 }
2321
2322 $result[ $slug ] = $value;
2323 }
2324 }
2325 return $result;
2326 }
2327
2328 /**
2329 * Similar to get_settings_values_by_slug, but doesn't compute the value.
2330 *
2331 * @since 5.9.0
2332 *
2333 * @param array $settings Settings to process.
2334 * @param array $preset_metadata One of the PRESETS_METADATA values.
2335 * @param array $origins List of origins to process.
2336 * @return array Array of presets where the key and value are both the slug.
2337 */
2338 protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
2339 if ( null === $origins ) {
2340 $origins = static::VALID_ORIGINS;
2341 }
2342
2343 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
2344
2345 $result = array();
2346 foreach ( $origins as $origin ) {
2347 if ( ! isset( $preset_per_origin[ $origin ] ) ) {
2348 continue;
2349 }
2350 foreach ( $preset_per_origin[ $origin ] as $preset ) {
2351 $slug = _wp_to_kebab_case( $preset['slug'] );
2352
2353 // Use the array as a set so we don't get duplicates.
2354 $result[ $slug ] = $slug;
2355 }
2356 }
2357 return $result;
2358 }
2359
2360 /**
2361 * Transforms a slug into a CSS Custom Property.
2362 *
2363 * @since 5.9.0
2364 *
2365 * @param string $input String to replace.
2366 * @param string $slug The slug value to use to generate the custom property.
2367 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
2368 */
2369 protected static function replace_slug_in_string( $input, $slug ) {
2370 return strtr( $input, array( '$slug' => $slug ) );
2371 }
2372
2373 /**
2374 * Given the block settings, extracts the CSS Custom Properties
2375 * for the presets and adds them to the $declarations array
2376 * following the format:
2377 *
2378 * ```php
2379 * array(
2380 * 'name' => 'property_name',
2381 * 'value' => 'property_value,
2382 * )
2383 * ```
2384 *
2385 * @since 5.8.0
2386 * @since 5.9.0 Added the `$origins` parameter.
2387 *
2388 * @param array $settings Settings to process.
2389 * @param array $origins List of origins to process.
2390 * @return array The modified $declarations.
2391 */
2392 protected static function compute_preset_vars( $settings, $origins ) {
2393 $declarations = array();
2394 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2395 if ( empty( $preset_metadata['css_vars'] ) ) {
2396 continue;
2397 }
2398
2399 $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
2400 foreach ( $values_by_slug as $slug => $value ) {
2401 $declarations[] = array(
2402 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
2403 'value' => $value,
2404 );
2405 }
2406 }
2407
2408 return $declarations;
2409 }
2410
2411 /**
2412 * Given an array of settings, extracts the CSS Custom Properties
2413 * for the custom values and adds them to the $declarations
2414 * array following the format:
2415 *
2416 * ```php
2417 * array(
2418 * 'name' => 'property_name',
2419 * 'value' => 'property_value,
2420 * )
2421 * ```
2422 *
2423 * @since 5.8.0
2424 *
2425 * @param array $settings Settings to process.
2426 * @return array The modified $declarations.
2427 */
2428 protected static function compute_theme_vars( $settings ) {
2429 $declarations = array();
2430 $custom_values = $settings['custom'] ?? array();
2431 $css_vars = static::flatten_tree( $custom_values );
2432 foreach ( $css_vars as $key => $value ) {
2433 $declarations[] = array(
2434 'name' => '--wp--custom--' . $key,
2435 'value' => $value,
2436 );
2437 }
2438
2439 return $declarations;
2440 }
2441
2442 /**
2443 * Given a tree, it creates a flattened one
2444 * by merging the keys and binding the leaf values
2445 * to the new keys.
2446 *
2447 * It also transforms camelCase names into kebab-case
2448 * and substitutes '/' by '-'.
2449 *
2450 * This is thought to be useful to generate
2451 * CSS Custom Properties from a tree,
2452 * although there's nothing in the implementation
2453 * of this function that requires that format.
2454 *
2455 * For example, assuming the given prefix is '--wp'
2456 * and the token is '--', for this input tree:
2457 *
2458 * {
2459 * 'some/property': 'value',
2460 * 'nestedProperty': {
2461 * 'sub-property': 'value'
2462 * }
2463 * }
2464 *
2465 * it'll return this output:
2466 *
2467 * {
2468 * '--wp--some-property': 'value',
2469 * '--wp--nested-property--sub-property': 'value'
2470 * }
2471 *
2472 * @since 5.8.0
2473 *
2474 * @param array $tree Input tree to process.
2475 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
2476 * @param string $token Optional. Token to use between levels. Default '--'.
2477 * @return array The flattened tree.
2478 */
2479 protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
2480 $result = array();
2481 foreach ( $tree as $property => $value ) {
2482 $new_key = $prefix . str_replace(
2483 '/',
2484 '-',
2485 strtolower( _wp_to_kebab_case( $property ) )
2486 );
2487
2488 if ( is_array( $value ) ) {
2489 $new_prefix = $new_key . $token;
2490 $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
2491 foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
2492 $result[ $subtree_key ] = $subtree_value;
2493 }
2494 } else {
2495 $result[ $new_key ] = $value;
2496 }
2497 }
2498 return $result;
2499 }
2500
2501 /**
2502 * Given a styles array, it extracts the style properties
2503 * and adds them to the $declarations array following the format:
2504 *
2505 * ```php
2506 * array(
2507 * 'name' => 'property_name',
2508 * 'value' => 'property_value',
2509 * )
2510 * ```
2511 *
2512 * @since 5.8.0
2513 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
2514 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
2515 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
2516 * @since 6.6.0 Passing current theme JSON settings to wp_get_typography_font_size_value(). Using style engine to correctly fetch background CSS values.
2517 * @since 6.7.0 Allow ref resolution of background properties.
2518 *
2519 * @param array $styles Styles to process.
2520 * @param array $settings Theme settings.
2521 * @param array $properties Properties metadata.
2522 * @param array $theme_json Theme JSON array.
2523 * @param string $selector The style block selector.
2524 * @param boolean $use_root_padding Whether to add custom properties at root level.
2525 * @return array Returns the modified $declarations.
2526 */
2527 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
2528 if ( empty( $styles ) ) {
2529 return array();
2530 }
2531
2532 if ( null === $properties ) {
2533 $properties = static::PROPERTIES_METADATA;
2534 }
2535 $declarations = array();
2536 $root_variable_duplicates = array();
2537 $root_style_length = strlen( '--wp--style--root--' );
2538
2539 foreach ( $properties as $css_property => $value_path ) {
2540 if ( ! is_array( $value_path ) ) {
2541 continue;
2542 }
2543
2544 $is_root_style = str_starts_with( $css_property, '--wp--style--root--' );
2545 if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
2546 continue;
2547 }
2548
2549 $value = static::get_property_value( $styles, $value_path, $theme_json );
2550
2551 // Root-level padding styles don't currently support strings with CSS shorthand values.
2552 // This may change: https://github.com/WordPress/gutenberg/issues/40132.
2553 if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
2554 continue;
2555 }
2556
2557 if ( $is_root_style && $use_root_padding ) {
2558 $root_variable_duplicates[] = substr( $css_property, $root_style_length );
2559 }
2560
2561 /*
2562 * Processes background image styles.
2563 * If the value is a URL, it will be converted to a CSS `url()` value.
2564 * For an uploaded image (images with a database ID), apply size and position
2565 * defaults equal to those applied in block supports in lib/background.php.
2566 */
2567 if ( 'background-image' === $css_property ) {
2568 $background_image_input = array();
2569 if ( ! empty( $value ) ) {
2570 $background_image_input['backgroundImage'] = $value;
2571 }
2572 $gradient_value = $styles['background']['gradient'] ?? null;
2573 if ( ! empty( $gradient_value ) ) {
2574 $background_image_input['gradient'] = $gradient_value;
2575 }
2576 if ( ! empty( $background_image_input ) ) {
2577 $background_styles = gutenberg_style_engine_get_styles(
2578 array( 'background' => $background_image_input )
2579 );
2580 $value = $background_styles['declarations'][ $css_property ] ?? null;
2581 }
2582 }
2583 if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
2584 if ( 'background-size' === $css_property ) {
2585 $value = 'cover';
2586 }
2587 // If the background size is set to `contain` and no position is set, set the position to `center`.
2588 if ( 'background-position' === $css_property ) {
2589 $background_size = $styles['background']['backgroundSize'] ?? null;
2590 $value = 'contain' === $background_size ? '50% 50%' : null;
2591 }
2592 }
2593
2594 // Skip if empty and not "0" or value represents array of longhand values.
2595 $has_missing_value = empty( $value ) && ! is_numeric( $value );
2596 if ( $has_missing_value || is_array( $value ) ) {
2597 continue;
2598 }
2599
2600 // Calculates fluid typography rules where available.
2601 if ( 'font-size' === $css_property ) {
2602 /*
2603 * wp_get_typography_font_size_value() will check
2604 * if fluid typography has been activated and also
2605 * whether the incoming value can be converted to a fluid value.
2606 * Values that already have a clamp() function will not pass the test,
2607 * and therefore the original $value will be returned.
2608 * Pass the current theme_json settings to override any global settings.
2609 */
2610 $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ), $settings );
2611 }
2612
2613 if ( 'aspect-ratio' === $css_property ) {
2614 // For aspect ratio to work, other dimensions rules must be unset.
2615 // This ensures that a fixed height does not override the aspect ratio.
2616 $declarations[] = array(
2617 'name' => 'min-height',
2618 'value' => 'unset',
2619 );
2620 }
2621
2622 $declarations[] = array(
2623 'name' => $css_property,
2624 'value' => $value,
2625 );
2626 }
2627
2628 // If a variable value is added to the root, the corresponding property should be removed.
2629 foreach ( $root_variable_duplicates as $duplicate ) {
2630 $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
2631 if ( is_numeric( $discard ) ) {
2632 array_splice( $declarations, $discard, 1 );
2633 }
2634 }
2635
2636 return $declarations;
2637 }
2638
2639 /**
2640 * Returns the style property for the given path.
2641 *
2642 * It also converts references to a path to the value
2643 * stored at that location, e.g.
2644 * { "ref": "style.color.background" } => "#fff".
2645 *
2646 * @since 5.8.0
2647 * @since 5.9.0 Added support for values of array type, which are returned as is.
2648 * @since 6.1.0 Added the `$theme_json` parameter.
2649 * @since 6.7.0 Added support for background image refs
2650 *
2651 * @param array $styles Styles subtree.
2652 * @param array $path Which property to process.
2653 * @param array $theme_json Theme JSON array.
2654 * @return string|array Style property value.
2655 */
2656 protected static function get_property_value( $styles, $path, $theme_json = null ) {
2657 $value = _wp_array_get( $styles, $path, '' );
2658
2659 // Gutenberg didn't have this check.
2660 if ( '' === $value || null === $value ) {
2661 // No need to process the value further.
2662 return '';
2663 }
2664
2665 /*
2666 * Where the current value is an array with a 'ref' key pointing
2667 * to a path, this converts that path into the value at that path.
2668 * For example: { "ref": "style.color.background" } => "#fff".
2669 */
2670 if ( is_array( $value ) && isset( $value['ref'] ) ) {
2671 $value_path = explode( '.', $value['ref'] );
2672 $ref_value = _wp_array_get( $theme_json, $value_path, null );
2673 // Background Image refs can refer to a string or an array containing a URL string.
2674 $ref_value_url = $ref_value['url'] ?? null;
2675 // Only use the ref value if we find anything.
2676 if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) {
2677 $value = $ref_value;
2678 }
2679
2680 if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
2681 $path_string = json_encode( $path );
2682 $ref_value_string = json_encode( $ref_value );
2683 _doing_it_wrong(
2684 'get_property_value',
2685 sprintf(
2686 /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
2687 __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.', 'gutenberg' ),
2688 'theme.json',
2689 $ref_value_string,
2690 $path_string,
2691 $ref_value['ref']
2692 ),
2693 '6.1.0'
2694 );
2695 }
2696 }
2697
2698 return $value;
2699 }
2700
2701 /**
2702 * Builds metadata for the setting nodes, which returns in the form of:
2703 *
2704 * [
2705 * [
2706 * 'path' => ['path', 'to', 'some', 'node' ],
2707 * 'selector' => 'CSS selector for some node'
2708 * ],
2709 * [
2710 * 'path' => [ 'path', 'to', 'other', 'node' ],
2711 * 'selector' => 'CSS selector for other node'
2712 * ],
2713 * ]
2714 *
2715 * @since 5.8.0
2716 *
2717 * @param array $theme_json The tree to extract setting nodes from.
2718 * @param array $selectors List of selectors per block.
2719 * @return array An array of setting nodes metadata.
2720 */
2721 protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
2722 $nodes = array();
2723 if ( ! isset( $theme_json['settings'] ) ) {
2724 return $nodes;
2725 }
2726
2727 // Top-level.
2728 $nodes[] = array(
2729 'path' => array( 'settings' ),
2730 'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
2731 );
2732
2733 // Calculate paths for blocks.
2734 if ( ! isset( $theme_json['settings']['blocks'] ) ) {
2735 return $nodes;
2736 }
2737
2738 foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
2739 $selector = null;
2740 if ( isset( $selectors[ $name ]['selector'] ) ) {
2741 $selector = $selectors[ $name ]['selector'];
2742 }
2743
2744 $nodes[] = array(
2745 'path' => array( 'settings', 'blocks', $name ),
2746 'selector' => $selector,
2747 'selectors' => $selectors[ $name ]['selectors'] ?? array(),
2748 );
2749 }
2750
2751 return $nodes;
2752 }
2753
2754 /**
2755 * Builds metadata for the style nodes, which returns in the form of:
2756 *
2757 * [
2758 * [
2759 * 'path' => [ 'path', 'to', 'some', 'node' ],
2760 * 'selector' => 'CSS selector for some node',
2761 * 'duotone' => 'CSS selector for duotone for some node'
2762 * ],
2763 * [
2764 * 'path' => ['path', 'to', 'other', 'node' ],
2765 * 'selector' => 'CSS selector for other node',
2766 * 'duotone' => null
2767 * ],
2768 * ]
2769 *
2770 * @since 5.8.0
2771 *
2772 * @param array $theme_json The tree to extract style nodes from.
2773 * @param array $selectors List of selectors per block.
2774 * @param array $options An array of options to facilitate filtering style node generation
2775 * The options currently supported are:
2776 * - `include_block_style_variations` which includes CSS for block style variations.
2777 * @return array An array of style nodes metadata.
2778 */
2779 protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) {
2780 $nodes = array();
2781 if ( ! isset( $theme_json['styles'] ) ) {
2782 return $nodes;
2783 }
2784
2785 // Top-level.
2786 $nodes[] = array(
2787 'path' => array( 'styles' ),
2788 'selector' => static::ROOT_BLOCK_SELECTOR,
2789 );
2790
2791 if ( isset( $theme_json['styles']['elements'] ) ) {
2792 foreach ( self::ELEMENTS as $element => $selector ) {
2793 if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) {
2794 continue;
2795 }
2796
2797 // Handle element defaults.
2798 $nodes[] = array(
2799 'path' => array( 'styles', 'elements', $element ),
2800 'selector' => static::ELEMENTS[ $element ],
2801 );
2802
2803 // Handle any pseudo selectors for the element.
2804 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
2805 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
2806
2807 if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
2808 $nodes[] = array(
2809 'path' => array( 'styles', 'elements', $element ),
2810 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
2811 );
2812 }
2813 }
2814 }
2815 }
2816 }
2817
2818 // Blocks.
2819 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
2820 return $nodes;
2821 }
2822
2823 $block_options = $options;
2824 if ( ! isset( $block_options['include_block_style_variations'] ) ) {
2825 $block_options['include_block_style_variations'] = true;
2826 }
2827 $block_nodes = static::get_block_nodes( $theme_json, $selectors, $block_options );
2828 foreach ( $block_nodes as $block_node ) {
2829 $nodes[] = $block_node;
2830 }
2831
2832 /**
2833 * Filters the list of style nodes with metadata.
2834 *
2835 * This allows for things like loading block CSS independently.
2836 *
2837 * @since 6.1.0
2838 *
2839 * @param array $nodes Style nodes with metadata.
2840 */
2841 return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
2842 }
2843
2844 /**
2845 * A public helper to get the block nodes from a theme.json file.
2846 *
2847 * @since 6.1.0
2848 *
2849 * @return array The block nodes in theme.json.
2850 */
2851 public function get_styles_block_nodes() {
2852 return static::get_block_nodes( $this->theme_json );
2853 }
2854
2855 /**
2856 * Returns a filtered declarations array if there is a separator block with only a background
2857 * style defined in theme.json by adding a color attribute to reflect the changes in the front.
2858 *
2859 * @since 6.1.1
2860 *
2861 * @param array $declarations List of declarations.
2862 * @return array $declarations List of declarations filtered.
2863 */
2864 private static function update_separator_declarations( $declarations ) {
2865 // Gutenberg and core implementation differed.
2866 // https://github.com/WordPress/gutenberg/pull/44943.
2867 $background_color = '';
2868 $border_color_matches = false;
2869 $text_color_matches = false;
2870
2871 foreach ( $declarations as $declaration ) {
2872 if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
2873 $background_color = $declaration['value'];
2874 } elseif ( 'border-color' === $declaration['name'] ) {
2875 $border_color_matches = true;
2876 } elseif ( 'color' === $declaration['name'] ) {
2877 $text_color_matches = true;
2878 }
2879
2880 if ( $background_color && $border_color_matches && $text_color_matches ) {
2881 break;
2882 }
2883 }
2884
2885 if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
2886 $declarations[] = array(
2887 'name' => 'color',
2888 'value' => $background_color,
2889 );
2890 }
2891
2892 return $declarations;
2893 }
2894
2895 /**
2896 * Updates button width declarations to use a calc() formula for percentage values.
2897 *
2898 * When a percentage width is set on the Button block via Global Styles, the
2899 * resulting CSS needs to account for block gap spacing so that buttons tile
2900 * correctly on a row (e.g. 4 buttons at 25% width all fit on one row).
2901 *
2902 * This mirrors the dynamic calc() formula applied at the block instance level
2903 * in the button block's stylesheet (style.scss).
2904 *
2905 * @since 7.1.0
2906 *
2907 * @param array $feature_declarations The feature declarations keyed by selector.
2908 * @param array $settings The theme.json settings.
2909 * @return array The updated feature declarations.
2910 */
2911 private static function update_button_width_declarations( $feature_declarations, $settings ) {
2912 if ( ! isset( $feature_declarations['.wp-block-button'] ) ) {
2913 return $feature_declarations;
2914 }
2915
2916 foreach ( $feature_declarations['.wp-block-button'] as &$declaration ) {
2917 if ( 'width' !== $declaration['name'] || ! isset( $declaration['value'] ) ) {
2918 continue;
2919 }
2920
2921 $value = $declaration['value'];
2922 $percentage = null;
2923
2924 // Case 1: Direct percentage value e.g. "25%".
2925 if ( is_string( $value ) && str_ends_with( $value, '%' ) ) {
2926 $percentage = (float) $value;
2927 }
2928
2929 // Case 2: Preset CSS var e.g. "var(--wp--preset--dimension--50)".
2930 if ( null === $percentage && is_string( $value ) && str_starts_with( $value, 'var(--wp--preset--dimension--' ) ) {
2931 // Extract the slug from the var name.
2932 $slug = substr( $value, strlen( 'var(--wp--preset--dimension--' ), -1 );
2933
2934 /*
2935 * Look up the preset size across all origins.
2936 * Check block-level settings first (core/button), then top-level settings.
2937 */
2938 $dimension_sizes = ( $settings['blocks']['core/button']['dimensions']['dimensionSizes'] ?? array() )
2939 + ( $settings['dimensions']['dimensionSizes'] ?? array() );
2940 foreach ( $dimension_sizes as $origin_sizes ) {
2941 if ( ! is_array( $origin_sizes ) ) {
2942 continue;
2943 }
2944 foreach ( $origin_sizes as $preset ) {
2945 if ( isset( $preset['slug'] ) && $slug === $preset['slug'] && isset( $preset['size'] ) ) {
2946 $size = $preset['size'];
2947 if ( is_string( $size ) && str_ends_with( $size, '%' ) ) {
2948 $percentage = (float) $size;
2949 }
2950 break 2;
2951 }
2952 }
2953 }
2954 }
2955
2956 if ( null === $percentage ) {
2957 continue;
2958 }
2959
2960 /*
2961 * Apply the same calc() formula as the block instance level (style.scss).
2962 * The numeric percentage value is used as a unitless number:
2963 * - Multiplied by 1% to get the percentage width.
2964 * - Divided by 100 to calculate the gap adjustment proportion.
2965 */
2966 $declaration['value'] = sprintf(
2967 'calc(%s * 1%% - (var(--wp--style--block-gap, 0.5em) * (1 - %s / 100)))',
2968 $percentage,
2969 $percentage
2970 );
2971 }
2972 unset( $declaration );
2973
2974 return $feature_declarations;
2975 }
2976
2977 /**
2978 * Updates the text indent selector for paragraph blocks based on the textIndent setting.
2979 *
2980 * The textIndent setting can be 'subsequent' (default), 'all', or false.
2981 * When set to 'all', the selector should be '.wp-block-paragraph' instead of
2982 * '.wp-block-paragraph + .wp-block-paragraph' to apply indent to all paragraphs.
2983 *
2984 * @since 7.0.0
2985 *
2986 * @param array $feature_declarations The feature declarations keyed by selector.
2987 * @param array $settings The theme.json settings.
2988 * @param string $block_name The block name being processed.
2989 * @return array The updated feature declarations.
2990 */
2991 private static function update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ) {
2992 if ( 'core/paragraph' !== $block_name ) {
2993 return $feature_declarations;
2994 }
2995
2996 // Check block-level settings first, then fall back to global settings.
2997 $block_settings = $settings['blocks']['core/paragraph'] ?? null;
2998 $text_indent_setting = $block_settings['typography']['textIndent']
2999 ?? $settings['typography']['textIndent']
3000 ?? 'subsequent';
3001
3002 if ( 'all' !== $text_indent_setting ) {
3003 return $feature_declarations;
3004 }
3005
3006 // Look for the text indent selector and replace it.
3007 $old_selector = '.wp-block-paragraph + .wp-block-paragraph';
3008 $new_selector = '.wp-block-paragraph';
3009
3010 if ( isset( $feature_declarations[ $old_selector ] ) ) {
3011 $declarations = $feature_declarations[ $old_selector ];
3012 unset( $feature_declarations[ $old_selector ] );
3013 $feature_declarations[ $new_selector ] = $declarations;
3014 }
3015
3016 return $feature_declarations;
3017 }
3018
3019 /**
3020 * An internal method to get the block nodes from a theme.json file.
3021 *
3022 * @since 6.1.0
3023 *
3024 * @param array $theme_json The theme.json converted to an array.
3025 * @param array $selectors Optional list of selectors per block.
3026 * @param array $options {
3027 * Optional. An array of options for now used for internal purposes only (may change without notice).
3028 *
3029 * @type bool $include_block_style_variations Includes nodes for block style variations. Default false.
3030 * @type bool $include_node_paths_only Return only block nodes node paths. Default false.
3031 * }
3032 * @return array The block nodes in theme.json.
3033 */
3034 private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
3035 $nodes = array();
3036
3037 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3038 return $nodes;
3039 }
3040
3041 $include_variations = $options['include_block_style_variations'] ?? false;
3042 $include_node_paths_only = $options['include_node_paths_only'] ?? false;
3043
3044 // If only node paths are to be returned, skip selector assignment.
3045 if ( ! $include_node_paths_only ) {
3046 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
3047 }
3048
3049 foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
3050 $node_path = array( 'styles', 'blocks', $name );
3051 if ( $include_node_paths_only ) {
3052 $variation_paths = array();
3053 if ( $include_variations && isset( $node['variations'] ) ) {
3054 foreach ( $node['variations'] as $variation => $variation_node ) {
3055 $variation_paths[] = array(
3056 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3057 );
3058 }
3059 }
3060 $node = array(
3061 'path' => $node_path,
3062 );
3063 if ( ! empty( $variation_paths ) ) {
3064 $node['variations'] = $variation_paths;
3065 }
3066 $nodes[] = $node;
3067 } else {
3068 $selector = null;
3069 if ( isset( $selectors[ $name ]['selector'] ) ) {
3070 $selector = $selectors[ $name ]['selector'];
3071 }
3072
3073 $duotone_selector = null;
3074 if ( isset( $selectors[ $name ]['duotone'] ) ) {
3075 $duotone_selector = $selectors[ $name ]['duotone'];
3076 }
3077
3078 $feature_selectors = null;
3079 if ( isset( $selectors[ $name ]['selectors'] ) ) {
3080 $feature_selectors = $selectors[ $name ]['selectors'];
3081 }
3082
3083 $variation_selectors = array();
3084
3085 if ( $include_variations && isset( $node['variations'] ) ) {
3086 foreach ( $node['variations'] as $variation => $node ) {
3087 $variation_selectors[] = array(
3088 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3089 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
3090 );
3091 }
3092 }
3093
3094 $nodes[] = array(
3095 'name' => $name,
3096 'path' => $node_path,
3097 'selector' => $selector,
3098 'selectors' => $feature_selectors,
3099 'duotone' => $duotone_selector,
3100 'variations' => $variation_selectors,
3101 'css' => $selector,
3102 );
3103
3104 // Handle any pseudo selectors for the block.
3105 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3106 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3107 if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) {
3108 /*
3109 * Append the pseudo-selector to each feature selector so that
3110 * get_feature_declarations_for_node generates CSS scoped to the
3111 * pseudo-state (e.g. '.wp-block-button:hover') rather than the
3112 * default state (e.g. '.wp-block-button').
3113 */
3114 $pseudo_feature_selectors = array();
3115 foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
3116 if ( is_array( $feature_selector ) ) {
3117 $pseudo_feature_selectors[ $feature ] = array();
3118 foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
3119 $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
3120 }
3121 } else {
3122 $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
3123 }
3124 }
3125
3126 $nodes[] = array(
3127 'name' => $name,
3128 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ),
3129 'selector' => static::append_to_selector( $selector, $pseudo_selector ),
3130 'selectors' => $pseudo_feature_selectors,
3131 'duotone' => $duotone_selector,
3132 'variations' => $variation_selectors,
3133 'css' => static::append_to_selector( $selector, $pseudo_selector ),
3134 );
3135 }
3136 }
3137 }
3138
3139 // Handle custom states (e.g. '@current' for navigation).
3140 if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3141 foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3142 if (
3143 isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
3144 isset( $selectors[ $name ]['states'][ $custom_state ] )
3145 ) {
3146 $custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
3147 $nodes[] = array(
3148 'name' => $name,
3149 'path' => array( 'styles', 'blocks', $name, $custom_state ),
3150 'selector' => $custom_css_selector,
3151 'selectors' => $feature_selectors,
3152 'duotone' => $duotone_selector,
3153 'variations' => $variation_selectors,
3154 'css' => $custom_css_selector,
3155 );
3156
3157 // Sub-pseudo-selectors within the custom state.
3158 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3159 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo ) {
3160 if ( isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ][ $pseudo ] ) ) {
3161 $compound_css_selector = static::append_to_selector( $custom_css_selector, $pseudo );
3162 $nodes[] = array(
3163 'name' => $name,
3164 'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
3165 'selector' => $compound_css_selector,
3166 'selectors' => $feature_selectors,
3167 'duotone' => $duotone_selector,
3168 'variations' => $variation_selectors,
3169 'css' => $compound_css_selector,
3170 );
3171 }
3172 }
3173 }
3174 }
3175 }
3176 }
3177 }
3178 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
3179 foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
3180 $node_path = array( 'styles', 'blocks', $name, 'elements', $element );
3181 if ( $include_node_paths_only ) {
3182 $nodes[] = array(
3183 'path' => $node_path,
3184 );
3185 continue;
3186 }
3187
3188 $nodes[] = array(
3189 'path' => $node_path,
3190 'selector' => $selectors[ $name ]['elements'][ $element ],
3191 );
3192
3193 // Handle any pseudo selectors for the element.
3194 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3195 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3196 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3197 $node_path = array( 'styles', 'blocks', $name, 'elements', $element );
3198 if ( $include_node_paths_only ) {
3199 $nodes[] = array(
3200 'path' => $node_path,
3201 );
3202 continue;
3203 }
3204
3205 $nodes[] = array(
3206 'path' => $node_path,
3207 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
3208 );
3209 }
3210 }
3211 }
3212 }
3213 }
3214 }
3215
3216 return $nodes;
3217 }
3218
3219 /**
3220 * Gets the CSS rules for a particular block from theme.json.
3221 *
3222 * @since 6.1.0
3223 * @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image.
3224 *
3225 * @param array $block_metadata Metadata about the block to get styles for.
3226 *
3227 * @return string Styles for the block.
3228 */
3229 public function get_styles_for_block( $block_metadata ) {
3230 $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3231 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3232 $selector = $block_metadata['selector'];
3233 $settings = $this->theme_json['settings'] ?? null;
3234 $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector;
3235
3236 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
3237
3238 // Update text indent selector for paragraph blocks based on the textIndent setting.
3239 $block_name = $block_metadata['name'] ?? null;
3240 $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
3241
3242 // Update button width declarations for percentage values to use calc() with block gap.
3243 $feature_declarations = static::update_button_width_declarations( $feature_declarations, $settings );
3244
3245 // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
3246 $style_variation_declarations = array();
3247 $style_variation_custom_css = array();
3248 $style_variation_layout_metadata = array();
3249 if ( ! empty( $block_metadata['variations'] ) ) {
3250 foreach ( $block_metadata['variations'] as $style_variation ) {
3251 $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3252 $clean_style_variation_selector = trim( $style_variation['selector'] );
3253
3254 // Generate any feature/subfeature style declarations for the current style variation.
3255 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
3256
3257 // Update text indent selector for paragraph blocks based on the textIndent setting.
3258 $variation_declarations = static::update_paragraph_text_indent_selector( $variation_declarations, $settings, $block_name );
3259
3260 // Update button width declarations for percentage values to use calc() with block gap.
3261 $variation_declarations = static::update_button_width_declarations( $variation_declarations, $settings );
3262
3263 // Combine selectors with style variation's selector and add to overall style variation declarations.
3264 foreach ( $variation_declarations as $current_selector => $new_declarations ) {
3265 /*
3266 * Clean up any whitespace between comma separated selectors.
3267 * This prevents these spaces breaking compound selectors such as:
3268 * - `.wp-block-list:not(.wp-block-list .wp-block-list)`
3269 * - `.wp-block-image img, .wp-block-image.my-class img`
3270 */
3271 $clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector );
3272 $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_current_selector );
3273
3274 // Prepend the variation selector to the current selector.
3275 $split_selectors = explode( ',', $shortened_selector );
3276 $updated_selectors = array_map(
3277 static function ( $split_selector ) use ( $clean_style_variation_selector ) {
3278 return $clean_style_variation_selector . $split_selector;
3279 },
3280 $split_selectors
3281 );
3282 $combined_selectors = implode( ',', $updated_selectors );
3283
3284 // Add the new declarations to the overall results under the modified selector.
3285 $style_variation_declarations[ $combined_selectors ] = $new_declarations;
3286 }
3287 // Compute declarations for remaining styles not covered by feature level selectors.
3288 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
3289
3290 // Process pseudo-selectors for this variation (e.g., :hover, :focus).
3291 $block_name = $block_metadata['name'] ?? ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ? static::get_block_name_from_metadata_path( $block_metadata ) : null );
3292 $variation_pseudo_declarations = static::process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name );
3293 $style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
3294
3295 // Store custom CSS for the style variation.
3296 if ( isset( $style_variation_node['css'] ) ) {
3297 $style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
3298 }
3299
3300 // Store variation metadata and node for layout styles generation.
3301 // Only store if the variation has blockGap defined.
3302 if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3303 // Append block selector to the variation selector for proper targeting.
3304 $variation_metadata_with_selector = $style_variation;
3305 $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3306 $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3307 'metadata' => $variation_metadata_with_selector,
3308 'node' => $style_variation_node,
3309 );
3310 }
3311 }
3312 }
3313
3314 /*
3315 * Get a reference to element name from path.
3316 * $block_metadata['path'] = array( 'styles','elements','link' );
3317 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
3318 * Skip non-element paths like just ['styles'].
3319 */
3320 $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
3321
3322 $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
3323
3324 $element_pseudo_allowed = array();
3325
3326 if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
3327 $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
3328 }
3329
3330 /*
3331 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
3332 * This also resets the array keys.
3333 */
3334 $pseudo_matches = array_values(
3335 array_filter(
3336 $element_pseudo_allowed,
3337 static function ( $pseudo_selector ) use ( $selector ) {
3338 /*
3339 * Check if the pseudo selector is in the current selector,
3340 * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible).
3341 */
3342 return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1;
3343 }
3344 )
3345 );
3346
3347 $pseudo_selector = $pseudo_matches[0] ?? null;
3348
3349 /*
3350 * If the current selector is a pseudo selector that's defined in the allow list for the current
3351 * element then compute the style properties for it.
3352 * Otherwise just compute the styles for the default selector as normal.
3353 */
3354 if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
3355 isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
3356 && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
3357 ) {
3358 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
3359 } else {
3360 /*
3361 * For block pseudo-selector nodes (e.g. ':hover'), $node has already had any
3362 * feature-selector properties (e.g. writingMode) removed by get_feature_declarations_for_node,
3363 * so those properties are not output twice.
3364 */
3365 $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
3366 }
3367
3368 $block_rules = '';
3369
3370 /*
3371 * 1. Bespoke declaration modifiers:
3372 * - 'filter': Separate the declarations that use the general selector
3373 * from the ones using the duotone selector.
3374 * - 'background|background-image': set the html min-height to 100%
3375 * to ensure the background covers the entire viewport.
3376 *
3377 */
3378 $declarations_duotone = array();
3379 $should_set_root_min_height = false;
3380
3381 foreach ( $declarations as $index => $declaration ) {
3382 if ( 'filter' === $declaration['name'] ) {
3383 /*
3384 * 'unset' filters happen when a filter is unset
3385 * in the site-editor UI. Because the 'unset' value
3386 * in the user origin overrides the value in the
3387 * theme origin, we can skip rendering anything
3388 * here as no filter needs to be applied anymore.
3389 * So only add declarations to with values other
3390 * than 'unset'.
3391 */
3392 if ( 'unset' !== $declaration['value'] ) {
3393 $declarations_duotone[] = $declaration;
3394 }
3395 unset( $declarations[ $index ] );
3396 }
3397
3398 if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) {
3399 $should_set_root_min_height = true;
3400 }
3401 }
3402
3403 /*
3404 * If root styles has a background-image or a background (gradient) set,
3405 * set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view.
3406 * Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly,
3407 * and matches the behavior of the site editor.
3408 */
3409 if ( $should_set_root_min_height ) {
3410 $block_rules .= static::to_ruleset(
3411 'html',
3412 array(
3413 array(
3414 'name' => 'min-height',
3415 'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))',
3416 ),
3417 )
3418 );
3419 }
3420
3421 // Update declarations if there are separators with only background color defined.
3422 if ( '.wp-block-separator' === $selector ) {
3423 $declarations = static::update_separator_declarations( $declarations );
3424 }
3425
3426 /*
3427 * Root selector (body) styles should not be wrapped in `:root where()` to keep
3428 * specificity at (0,0,1) and maintain backwards compatibility.
3429 *
3430 * Top-level element styles using element-only specificity selectors should
3431 * not get wrapped in `:root :where()` to maintain backwards compatibility.
3432 *
3433 * Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
3434 * still need to be wrapped in `:root :where` to cap specificity for nested
3435 * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
3436 */
3437 $element_only_selector = $is_root_selector || (
3438 $current_element &&
3439 isset( static::ELEMENTS[ $current_element ] ) &&
3440 // buttons, captions etc. still need `:root :where()` as they are class based selectors.
3441 ! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) &&
3442 static::ELEMENTS[ $current_element ] === $selector
3443 );
3444
3445 // 2. Generate and append the rules that use the general selector.
3446 $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
3447 $block_rules .= static::to_ruleset( $general_selector, $declarations );
3448
3449 // 3. Generate and append the rules that use the duotone selector.
3450 if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
3451 $block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
3452 }
3453
3454 // 4. Generate Layout block gap styles.
3455 if (
3456 ! $is_root_selector &&
3457 ! empty( $block_metadata['name'] )
3458 ) {
3459 $block_rules .= $this->get_layout_styles( $block_metadata );
3460 }
3461
3462 // 5. Generate and append the feature level rulesets.
3463 foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
3464 $block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations );
3465 }
3466
3467 // 6. Generate and append the style variation rulesets.
3468 foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
3469 $block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations );
3470 if ( isset( $style_variation_layout_metadata[ $style_variation_selector ] ) ) {
3471 $variation_data = $style_variation_layout_metadata[ $style_variation_selector ];
3472 $block_rules .= $this->get_layout_styles( $variation_data['metadata'], array( 'node' => $variation_data['node'] ) );
3473 }
3474 if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
3475 $block_rules .= $style_variation_custom_css[ $style_variation_selector ];
3476 }
3477 }
3478
3479 // 7. Generate and append any custom CSS rules.
3480 if ( isset( $node['css'] ) && ! $is_root_selector ) {
3481 $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
3482 if ( is_array( $css_feature_selector ) ) {
3483 $css_feature_selector = $css_feature_selector['root'] ?? null;
3484 }
3485 $css_selector = is_string( $css_feature_selector ) ? $css_feature_selector : $selector;
3486 $block_rules .= $this->process_blocks_custom_css( $node['css'], $css_selector );
3487 }
3488
3489 return $block_rules;
3490 }
3491
3492 /**
3493 * Outputs the CSS for layout rules on the root.
3494 *
3495 * @since 6.1.0
3496 * @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties.
3497 *
3498 * @param string $selector The root node selector.
3499 * @param array $block_metadata The metadata for the root block.
3500 * @param array $options Optional. An array of options. Default empty array.
3501 * @return string The additional root rules CSS.
3502 */
3503 public function get_root_layout_rules( $selector, $block_metadata, $options = array() ) {
3504 $css = '';
3505 $settings = $this->theme_json['settings'] ?? array();
3506 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3507
3508 /*
3509 * If there are content and wide widths in theme.json, output them
3510 * as custom properties on the body element so all blocks can use them.
3511 */
3512 if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
3513 $content_size = $settings['layout']['contentSize'] ?? $settings['layout']['wideSize'];
3514 $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
3515 $wide_size = $settings['layout']['wideSize'] ?? $settings['layout']['contentSize'];
3516 $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
3517 $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
3518 $css .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
3519 }
3520
3521 /*
3522 * Reset default browser margin on the body element.
3523 * This is set on the body selector **before** generating the ruleset
3524 * from the `theme.json`. This is to ensure that if the `theme.json` declares
3525 * `margin` in its `spacing` declaration for the `body` element then these
3526 * user-generated values take precedence in the CSS cascade.
3527 * @link https://github.com/WordPress/gutenberg/issues/36147.
3528 */
3529 $css .= ':where(body) { margin: 0; }';
3530
3531 if ( $use_root_padding ) {
3532 // Top and bottom padding are applied to the outer block container.
3533 $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
3534 // Right and left padding are applied to the first container with `.has-global-padding` class.
3535 $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
3536 // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
3537 $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
3538 // Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container.
3539 $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
3540 // Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
3541 $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }';
3542 }
3543
3544 // Skip outputting alignment styles when base_layout_styles is enabled.
3545 // These styles target .wp-site-blocks which is only used by block themes.
3546 if ( empty( $options['base_layout_styles'] ) ) {
3547 $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
3548 $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
3549 $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
3550 }
3551
3552 // Block gap styles will be output unless explicitly set to `null`.
3553 if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
3554 $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
3555 $css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
3556 $css .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
3557 $css .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
3558
3559 // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
3560 $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
3561 }
3562 $css .= $this->get_layout_styles( $block_metadata, $options );
3563
3564 return $css;
3565 }
3566
3567 /**
3568 * For metadata values that can either be booleans or paths to booleans, gets the value.
3569 *
3570 * ```php
3571 * $data = array(
3572 * 'color' => array(
3573 * 'defaultPalette' => true
3574 * )
3575 * );
3576 *
3577 * static::get_metadata_boolean( $data, false );
3578 * // => false
3579 *
3580 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
3581 * // => true
3582 * ```
3583 *
3584 * @since 6.0.0
3585 *
3586 * @param array $data The data to inspect.
3587 * @param bool|array $path Boolean or path to a boolean.
3588 * @param bool $default_value Default value if the referenced path is missing.
3589 * Default false.
3590 * @return bool Value of boolean metadata.
3591 */
3592 protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
3593 if ( is_bool( $path ) ) {
3594 return $path;
3595 }
3596
3597 if ( is_array( $path ) ) {
3598 $value = _wp_array_get( $data, $path );
3599 if ( null !== $value ) {
3600 return $value;
3601 }
3602 }
3603
3604 return $default_value;
3605 }
3606
3607 /**
3608 * Merges new incoming data.
3609 *
3610 * @since 5.8.0
3611 * @since 5.9.0 Duotone preset also has origins.
3612 * @since 6.6.0 Use the spacingScale keyed by origin, and re-generate the
3613 * spacingSizes from spacingScale.
3614 *
3615 * @param WP_Theme_JSON_Gutenberg $incoming Data to merge.
3616 */
3617 public function merge( $incoming ) {
3618 $incoming_data = $incoming->get_raw_data();
3619 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
3620
3621 /*
3622 * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
3623 * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
3624 * we can allow partial spacingScale data to inherit missing data from earlier layers when
3625 * computing the spacing sizes.
3626 *
3627 * This happens before the presets are merged to ensure that default spacing sizes can be
3628 * removed from the theme origin if $prevent_override is true.
3629 */
3630 $flattened_spacing_scale = array();
3631 foreach ( static::VALID_ORIGINS as $origin ) {
3632 $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
3633
3634 // Apply the base spacing scale to the current layer.
3635 $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() );
3636 $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
3637
3638 $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
3639 if ( ! isset( $spacing_scale ) ) {
3640 continue;
3641 }
3642
3643 // Allow partial scale settings by merging with lower layers.
3644 $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
3645
3646 // Generate and merge the scales for this layer.
3647 $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
3648 $spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() );
3649 $spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale );
3650 $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
3651
3652 _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
3653 }
3654
3655 /*
3656 * The array_replace_recursive algorithm merges at the leaf level,
3657 * but we don't want leaf arrays to be merged, so we overwrite it.
3658 *
3659 * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
3660 * We rather replace the existing with the incoming value, if it exists.
3661 * This is the case of spacing.units.
3662 *
3663 * For leaf values that are associative arrays it will merge them as expected.
3664 * This is also not the behavior we want for the current associative arrays (presets).
3665 * We rather replace the existing with the incoming value, if it exists.
3666 * This happens, for example, when we merge data from theme.json upon existing
3667 * theme supports or when we merge anything coming from the same source twice.
3668 * This is the case of color.palette, color.gradients, color.duotone,
3669 * typography.fontSizes, or typography.fontFamilies.
3670 *
3671 * Additionally, for some preset types, we also want to make sure the
3672 * values they introduce don't conflict with default values. We do so
3673 * by checking the incoming slugs for theme presets and compare them
3674 * with the equivalent default presets: if a slug is present as a default
3675 * we remove it from the theme presets.
3676 */
3677 $nodes = static::get_setting_nodes( $incoming_data );
3678 $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
3679 foreach ( $nodes as $node ) {
3680 // Replace the spacing.units.
3681 $path = $node['path'];
3682 $path[] = 'spacing';
3683 $path[] = 'units';
3684
3685 $content = _wp_array_get( $incoming_data, $path, null );
3686 if ( isset( $content ) ) {
3687 _wp_array_set( $this->theme_json, $path, $content );
3688 }
3689
3690 // Replace the presets.
3691 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
3692 $prevent_override = $preset_metadata['prevent_override'];
3693 if ( is_array( $prevent_override ) ) {
3694 $global_path = array_merge( array( 'settings' ), $prevent_override );
3695 $global_value = _wp_array_get( $this->theme_json, $global_path, null );
3696
3697 $node_level_path = array_merge( $node['path'], $prevent_override );
3698 $prevent_override = _wp_array_get( $this->theme_json, $node_level_path, $global_value );
3699 }
3700
3701 foreach ( static::VALID_ORIGINS as $origin ) {
3702 $base_path = $node['path'];
3703 foreach ( $preset_metadata['path'] as $leaf ) {
3704 $base_path[] = $leaf;
3705 }
3706
3707 $path = $base_path;
3708 $path[] = $origin;
3709
3710 $content = _wp_array_get( $incoming_data, $path, null );
3711 if ( ! isset( $content ) ) {
3712 continue;
3713 }
3714
3715 // Set names for theme presets based on the slug if they are not set and can use default names.
3716 if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
3717 foreach ( $content as $key => $item ) {
3718 if ( ! isset( $item['name'] ) ) {
3719 $name = static::get_name_from_defaults( $item['slug'], $base_path );
3720 if ( null !== $name ) {
3721 $content[ $key ]['name'] = $name;
3722 }
3723 }
3724 }
3725 }
3726
3727 // Filter out default slugs from theme presets when defaults should not be overridden.
3728 if ( 'theme' === $origin && $prevent_override ) {
3729 $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
3730 $preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
3731 $preset_node = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
3732 $preset_slugs = array_merge_recursive( $preset_global, $preset_node );
3733
3734 $content = static::filter_slugs( $content, $preset_slugs );
3735 }
3736
3737 _wp_array_set( $this->theme_json, $path, $content );
3738 }
3739 }
3740 }
3741
3742 /*
3743 * Style values are merged at the leaf level, however
3744 * some values provide exceptions, namely style values that are
3745 * objects and represent unique definitions for the style.
3746 */
3747 $style_nodes = static::get_block_nodes(
3748 $this->theme_json,
3749 array(),
3750 array( 'include_node_paths_only' => true )
3751 );
3752
3753 // Add top-level styles.
3754 $style_nodes[] = array( 'path' => array( 'styles' ) );
3755
3756 foreach ( $style_nodes as $style_node ) {
3757 $path = $style_node['path'];
3758 /*
3759 * Background image styles should be replaced, not merged,
3760 * as they themselves are specific object definitions for the style.
3761 */
3762 $background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] );
3763 $content = _wp_array_get( $incoming_data, $background_image_path, null );
3764 if ( isset( $content ) ) {
3765 _wp_array_set( $this->theme_json, $background_image_path, $content );
3766 }
3767 }
3768 }
3769
3770 /**
3771 * Converts all filter (duotone) presets into SVGs.
3772 *
3773 * @since 5.9.1
3774 *
3775 * @param array $origins List of origins to process.
3776 * @return string SVG filters.
3777 */
3778 public function get_svg_filters( $origins ) {
3779 $blocks_metadata = static::get_blocks_metadata();
3780 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
3781
3782 $filters = '';
3783 foreach ( $setting_nodes as $metadata ) {
3784 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
3785 if ( empty( $node['color']['duotone'] ) ) {
3786 continue;
3787 }
3788
3789 $duotone_presets = $node['color']['duotone'];
3790
3791 foreach ( $origins as $origin ) {
3792 if ( ! isset( $duotone_presets[ $origin ] ) ) {
3793 continue;
3794 }
3795 foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
3796 $filters .= wp_get_duotone_filter_svg( $duotone_preset );
3797 }
3798 }
3799 }
3800
3801 return $filters;
3802 }
3803
3804 /**
3805 * Determines whether a presets should be overridden or not.
3806 *
3807 * @since 5.9.0
3808 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
3809 *
3810 * @param array $theme_json The theme.json like structure to inspect.
3811 * @param array $path Path to inspect.
3812 * @param bool|array $override Data to compute whether to override the preset.
3813 * @return boolean
3814 */
3815 protected static function should_override_preset( $theme_json, $path, $override ) {
3816 _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
3817
3818 if ( is_bool( $override ) ) {
3819 return $override;
3820 }
3821
3822 /*
3823 * The relationship between whether to override the defaults
3824 * and whether the defaults are enabled is inverse:
3825 *
3826 * - If defaults are enabled => theme presets should not be overridden
3827 * - If defaults are disabled => theme presets should be overridden
3828 *
3829 * For example, a theme sets defaultPalette to false,
3830 * making the default palette hidden from the user.
3831 * In that case, we want all the theme presets to be present,
3832 * so they should override the defaults.
3833 */
3834 if ( is_array( $override ) ) {
3835 $value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
3836 if ( isset( $value ) ) {
3837 return ! $value;
3838 }
3839
3840 // Search the top-level key if none was found for this node.
3841 $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
3842 if ( isset( $value ) ) {
3843 return ! $value;
3844 }
3845
3846 return true;
3847 }
3848
3849 return false;
3850 }
3851
3852 /**
3853 * Returns the default slugs for all the presets in an associative array
3854 * whose keys are the preset paths and the leaves is the list of slugs.
3855 *
3856 * For example:
3857 *
3858 * array(
3859 * 'color' => array(
3860 * 'palette' => array( 'slug-1', 'slug-2' ),
3861 * 'gradients' => array( 'slug-3', 'slug-4' ),
3862 * ),
3863 * )
3864 *
3865 * @since 5.9.0
3866 *
3867 * @param array $data A theme.json like structure.
3868 * @param array $node_path The path to inspect. It's 'settings' by default.
3869 * @return array
3870 */
3871 protected static function get_default_slugs( $data, $node_path ) {
3872 $slugs = array();
3873
3874 foreach ( static::PRESETS_METADATA as $metadata ) {
3875 $path = $node_path;
3876 foreach ( $metadata['path'] as $leaf ) {
3877 $path[] = $leaf;
3878 }
3879 $path[] = 'default';
3880
3881 $preset = _wp_array_get( $data, $path, null );
3882 if ( ! isset( $preset ) ) {
3883 continue;
3884 }
3885
3886 $slugs_for_preset = array();
3887 foreach ( $preset as $item ) {
3888 if ( isset( $item['slug'] ) ) {
3889 $slugs_for_preset[] = $item['slug'];
3890 }
3891 }
3892
3893 _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
3894 }
3895
3896 return $slugs;
3897 }
3898
3899 /**
3900 * Gets a `default`'s preset name by a provided slug.
3901 *
3902 * @since 5.9.0
3903 *
3904 * @param string $slug The slug we want to find a match from default presets.
3905 * @param array $base_path The path to inspect. It's 'settings' by default.
3906 * @return string|null
3907 */
3908 protected function get_name_from_defaults( $slug, $base_path ) {
3909 $path = $base_path;
3910 $path[] = 'default';
3911 $default_content = _wp_array_get( $this->theme_json, $path, null );
3912 if ( ! $default_content ) {
3913 return null;
3914 }
3915 foreach ( $default_content as $item ) {
3916 if ( $slug === $item['slug'] ) {
3917 return $item['name'];
3918 }
3919 }
3920 return null;
3921 }
3922
3923 /**
3924 * Removes the preset values whose slug is equal to any of given slugs.
3925 *
3926 * @since 5.9.0
3927 *
3928 * @param array $node The node with the presets to validate.
3929 * @param array $slugs The slugs that should not be overridden.
3930 * @return array The new node.
3931 */
3932 protected static function filter_slugs( $node, $slugs ) {
3933 if ( empty( $slugs ) ) {
3934 return $node;
3935 }
3936
3937 $new_node = array();
3938 foreach ( $node as $value ) {
3939 if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
3940 $new_node[] = $value;
3941 }
3942 }
3943
3944 return $new_node;
3945 }
3946
3947 /**
3948 * Removes insecure data from theme.json.
3949 *
3950 * @since 5.9.0
3951 * @since 6.6.0 Added support for block style variation element styles and $origin parameter.
3952 *
3953 * @param array $theme_json Structure to sanitize.
3954 * @param string $origin Optional. What source of data this object represents.
3955 * One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
3956 * @return array Sanitized structure.
3957 */
3958 public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
3959 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
3960 $origin = 'theme';
3961 }
3962
3963 $sanitized = array();
3964
3965 $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json, $origin );
3966
3967 $blocks_metadata = static::get_blocks_metadata();
3968 $valid_block_names = array_keys( $blocks_metadata );
3969 $valid_element_names = array_keys( static::ELEMENTS );
3970 $valid_variations = static::get_valid_block_style_variations( $blocks_metadata );
3971
3972 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
3973
3974 $blocks_metadata = static::get_blocks_metadata();
3975 $style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
3976 $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
3977
3978 foreach ( $style_nodes as $metadata ) {
3979 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
3980 if ( empty( $input ) ) {
3981 continue;
3982 }
3983
3984 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
3985 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
3986 $output = $input;
3987 } else {
3988 $output = static::remove_insecure_styles( $input );
3989 }
3990
3991 /*
3992 * Get a reference to element name from path.
3993 * $metadata['path'] = array( 'styles', 'elements', 'link' );
3994 */
3995 $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
3996
3997 /*
3998 * $output is stripped of pseudo selectors. Re-add and process them
3999 * or insecure styles here.
4000 */
4001 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
4002 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
4003 if ( isset( $input[ $pseudo_selector ] ) ) {
4004 $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
4005 }
4006 }
4007 }
4008
4009 if ( ! empty( $output ) ) {
4010 _wp_array_set( $sanitized, $metadata['path'], $output );
4011 }
4012
4013 if ( isset( $metadata['variations'] ) ) {
4014 foreach ( $metadata['variations'] as $variation ) {
4015 $variation_input = _wp_array_get( $theme_json, $variation['path'], array() );
4016 if ( empty( $variation_input ) ) {
4017 continue;
4018 }
4019
4020 $variation_output = static::remove_insecure_styles( $variation_input );
4021
4022 if ( isset( $variation_input['blocks'] ) ) {
4023 $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'] );
4024 }
4025
4026 if ( isset( $variation_input['elements'] ) ) {
4027 $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] );
4028 }
4029
4030 if ( ! empty( $variation_output ) ) {
4031 _wp_array_set( $sanitized, $variation['path'], $variation_output );
4032 }
4033 }
4034 }
4035 }
4036
4037 $setting_nodes = static::get_setting_nodes( $theme_json );
4038 foreach ( $setting_nodes as $metadata ) {
4039 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4040 if ( empty( $input ) ) {
4041 continue;
4042 }
4043
4044 $output = static::remove_insecure_settings( $input );
4045 if ( ! empty( $output ) ) {
4046 _wp_array_set( $sanitized, $metadata['path'], $output );
4047 }
4048 }
4049
4050 if ( empty( $sanitized['styles'] ) ) {
4051 unset( $theme_json['styles'] );
4052 } else {
4053 $theme_json['styles'] = $sanitized['styles'];
4054 }
4055
4056 if ( empty( $sanitized['settings'] ) ) {
4057 unset( $theme_json['settings'] );
4058 } else {
4059 $theme_json['settings'] = $sanitized['settings'];
4060 }
4061
4062 return $theme_json;
4063 }
4064
4065 /**
4066 * Remove insecure element styles within a variation or block.
4067 *
4068 * @since 6.8.0
4069 *
4070 * @param array $elements The elements to process.
4071 * @return array The sanitized elements styles.
4072 */
4073 protected static function remove_insecure_element_styles( $elements ) {
4074 $sanitized = array();
4075 $valid_element_names = array_keys( static::ELEMENTS );
4076
4077 foreach ( $valid_element_names as $element_name ) {
4078 $element_input = $elements[ $element_name ] ?? null;
4079 if ( $element_input ) {
4080 $element_output = static::remove_insecure_styles( $element_input );
4081
4082 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4083 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4084 if ( isset( $element_input[ $pseudo_selector ] ) ) {
4085 $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] );
4086 }
4087 }
4088 }
4089
4090 $sanitized[ $element_name ] = $element_output;
4091 }
4092 }
4093 return $sanitized;
4094 }
4095
4096 /**
4097 * Remove insecure styles from inner blocks and their elements.
4098 *
4099 * @since 6.8.0
4100 *
4101 * @param array $blocks The block styles to process.
4102 * @return array Sanitized block type styles.
4103 */
4104 protected static function remove_insecure_inner_block_styles( $blocks ) {
4105 $sanitized = array();
4106 foreach ( $blocks as $block_type => $block_input ) {
4107 $block_output = static::remove_insecure_styles( $block_input );
4108
4109 if ( isset( $block_input['elements'] ) ) {
4110 $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] );
4111 }
4112
4113 $sanitized[ $block_type ] = $block_output;
4114 }
4115 return $sanitized;
4116 }
4117
4118 /**
4119 * Preserves valid typed settings from input to output based on type markers in schema.
4120 *
4121 * Recursively iterates through the schema and validates/preserves settings
4122 * that have type markers (e.g., boolean) in VALID_SETTINGS.
4123 *
4124 * @since 7.0.0
4125 *
4126 * @param array $input Input settings to process.
4127 * @param array $output Output settings array (passed by reference).
4128 * @param array $schema Schema to validate against (typically VALID_SETTINGS).
4129 * @param array<string|int> $path Current path in the schema (for recursive calls).
4130 */
4131 private static function preserve_valid_typed_settings( $input, &$output, $schema, $path = array() ) {
4132 foreach ( $schema as $key => $schema_value ) {
4133 $current_path = array_merge( $path, array( $key ) );
4134
4135 // Validate boolean type markers.
4136 if ( is_bool( $schema_value ) ) {
4137 $value = _wp_array_get( $input, $current_path, null );
4138 if ( null !== $value && is_bool( $value ) ) {
4139 _wp_array_set( $output, $current_path, $value ); // Preserve boolean value.
4140 }
4141 } elseif ( is_array( $schema_value ) ) {
4142 self::preserve_valid_typed_settings( $input, $output, $schema_value, $current_path ); // Recurse into nested structure.
4143 }
4144 }
4145 }
4146
4147 /**
4148 * Processes a setting node and returns the same node
4149 * without the insecure settings.
4150 *
4151 * @since 5.9.0
4152 *
4153 * @param array $input Node to process.
4154 * @return array
4155 */
4156 protected static function remove_insecure_settings( $input ) {
4157 $output = array();
4158 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4159 foreach ( static::VALID_ORIGINS as $origin ) {
4160 $path_with_origin = $preset_metadata['path'];
4161 $path_with_origin[] = $origin;
4162 $presets = _wp_array_get( $input, $path_with_origin, null );
4163 if ( null === $presets ) {
4164 continue;
4165 }
4166
4167 $escaped_preset = array();
4168 foreach ( $presets as $preset ) {
4169 if (
4170 esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
4171 sanitize_html_class( $preset['slug'] ) === $preset['slug']
4172 ) {
4173 $value = null;
4174 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
4175 $value = $preset[ $preset_metadata['value_key'] ];
4176 } elseif (
4177 isset( $preset_metadata['value_func'] ) &&
4178 is_callable( $preset_metadata['value_func'] )
4179 ) {
4180 $value = call_user_func( $preset_metadata['value_func'], $preset );
4181 }
4182
4183 $preset_is_valid = true;
4184 foreach ( $preset_metadata['properties'] as $property ) {
4185 if ( ! static::is_safe_css_declaration( $property, $value ) ) {
4186 $preset_is_valid = false;
4187 break;
4188 }
4189 }
4190
4191 if ( $preset_is_valid ) {
4192 $escaped_preset[] = $preset;
4193 }
4194 }
4195 }
4196
4197 if ( ! empty( $escaped_preset ) ) {
4198 _wp_array_set( $output, $path_with_origin, $escaped_preset );
4199 }
4200 }
4201 }
4202
4203 // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
4204 static::remove_indirect_properties( $input, $output );
4205
4206 // Preserve all valid settings that have type markers in VALID_SETTINGS.
4207 self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
4208
4209 return $output;
4210 }
4211
4212 /**
4213 * Processes a style node and returns the same node
4214 * without the insecure styles.
4215 *
4216 * @since 5.9.0
4217 * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`.
4218 *
4219 * @param array $input Node to process.
4220 * @return array
4221 */
4222 protected static function remove_insecure_styles( $input ) {
4223 $output = array();
4224 $declarations = static::compute_style_properties( $input );
4225
4226 foreach ( $declarations as $declaration ) {
4227 if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
4228 $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
4229
4230 // Check the value isn't an array before adding so as to not
4231 // double up shorthand and longhand styles.
4232 $value = _wp_array_get( $input, $path, array() );
4233 if ( ! is_array( $value ) ) {
4234 _wp_array_set( $output, $path, $value );
4235 }
4236 }
4237 }
4238
4239 // Ensure indirect properties not handled by `compute_style_properties` are allowed.
4240 static::remove_indirect_properties( $input, $output );
4241
4242 return $output;
4243 }
4244
4245 /**
4246 * Checks that a declaration provided by the user is safe.
4247 *
4248 * @since 5.9.0
4249 *
4250 * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`.
4251 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
4252 * @return bool
4253 */
4254 protected static function is_safe_css_declaration( $property_name, $property_value ) {
4255 $style_to_validate = $property_name . ': ' . $property_value;
4256 $filtered = esc_html( safecss_filter_attr( $style_to_validate ) );
4257 return ! empty( trim( $filtered ) );
4258 }
4259
4260 /**
4261 * Removes indirect properties from the given input node and
4262 * sets in the given output node.
4263 *
4264 * @since 6.2.0
4265 *
4266 * @param array $input Node to process.
4267 * @param array $output The processed node. Passed by reference.
4268 */
4269 private static function remove_indirect_properties( $input, &$output ) {
4270 foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
4271 foreach ( $paths as $path ) {
4272 $value = _wp_array_get( $input, $path );
4273 if (
4274 is_string( $value ) &&
4275 static::is_safe_css_declaration( $property, $value )
4276 ) {
4277 _wp_array_set( $output, $path, $value );
4278 }
4279 }
4280 }
4281 }
4282
4283 /**
4284 * Returns the raw data.
4285 *
4286 * @since 5.8.0
4287 *
4288 * @return array Raw data.
4289 */
4290 public function get_raw_data() {
4291 return $this->theme_json;
4292 }
4293
4294 /**
4295 * Transforms the given editor settings according the
4296 * add_theme_support format to the theme.json format.
4297 *
4298 * @since 5.8.0
4299 *
4300 * @param array $settings Existing editor settings.
4301 * @return array Config that adheres to the theme.json schema.
4302 */
4303 public static function get_from_editor_settings( $settings ) {
4304 $theme_settings = array(
4305 'version' => static::LATEST_SCHEMA,
4306 'settings' => array(),
4307 );
4308
4309 // Deprecated theme supports.
4310 if ( isset( $settings['disableCustomColors'] ) ) {
4311 $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
4312 }
4313
4314 if ( isset( $settings['disableCustomGradients'] ) ) {
4315 $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
4316 }
4317
4318 if ( isset( $settings['disableCustomFontSizes'] ) ) {
4319 $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
4320 }
4321
4322 if ( isset( $settings['enableCustomLineHeight'] ) ) {
4323 $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
4324 }
4325
4326 if ( isset( $settings['enableCustomUnits'] ) ) {
4327 $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
4328 array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
4329 $settings['enableCustomUnits'];
4330 }
4331
4332 if ( isset( $settings['colors'] ) ) {
4333 $theme_settings['settings']['color']['palette'] = $settings['colors'];
4334 }
4335
4336 if ( isset( $settings['gradients'] ) ) {
4337 $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
4338 }
4339
4340 if ( isset( $settings['fontSizes'] ) ) {
4341 $font_sizes = $settings['fontSizes'];
4342 // Back-compatibility for presets without units.
4343 foreach ( $font_sizes as $key => $font_size ) {
4344 if ( is_numeric( $font_size['size'] ) ) {
4345 $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
4346 }
4347 }
4348 $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
4349 }
4350
4351 if ( isset( $settings['enableCustomSpacing'] ) ) {
4352 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
4353 }
4354
4355 if ( isset( $settings['spacingSizes'] ) ) {
4356 $theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes'];
4357 }
4358
4359 return $theme_settings;
4360 }
4361
4362 /**
4363 * Returns the current theme's wanted patterns(slugs) to be
4364 * registered from Pattern Directory.
4365 *
4366 * @since 6.0.0
4367 *
4368 * @return string[]
4369 */
4370 public function get_patterns() {
4371 if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
4372 return $this->theme_json['patterns'];
4373 }
4374 return array();
4375 }
4376
4377 /**
4378 * Returns a valid theme.json as provided by a theme.
4379 *
4380 * Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
4381 * This also uses appearanceTools instead of their opt-ins if all of them are true.
4382 *
4383 * @since 6.0.0
4384 *
4385 * @return array
4386 */
4387 public function get_data() {
4388 $output = $this->theme_json;
4389 $nodes = static::get_setting_nodes( $output );
4390
4391 /**
4392 * Flatten the theme & custom origins into a single one.
4393 *
4394 * For example, the following:
4395 *
4396 * {
4397 * "settings": {
4398 * "color": {
4399 * "palette": {
4400 * "theme": [ {} ],
4401 * "custom": [ {} ]
4402 * }
4403 * }
4404 * }
4405 * }
4406 *
4407 * will be converted to:
4408 *
4409 * {
4410 * "settings": {
4411 * "color": {
4412 * "palette": [ {} ]
4413 * }
4414 * }
4415 * }
4416 */
4417 foreach ( $nodes as $node ) {
4418 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4419 $path = $node['path'];
4420 foreach ( $preset_metadata['path'] as $preset_metadata_path ) {
4421 $path[] = $preset_metadata_path;
4422 }
4423 $preset = _wp_array_get( $output, $path, null );
4424 if ( null === $preset ) {
4425 continue;
4426 }
4427
4428 $items = array();
4429 if ( isset( $preset['theme'] ) ) {
4430 foreach ( $preset['theme'] as $item ) {
4431 $slug = $item['slug'];
4432 unset( $item['slug'] );
4433 $items[ $slug ] = $item;
4434 }
4435 }
4436 if ( isset( $preset['custom'] ) ) {
4437 foreach ( $preset['custom'] as $item ) {
4438 $slug = $item['slug'];
4439 unset( $item['slug'] );
4440 $items[ $slug ] = $item;
4441 }
4442 }
4443 $flattened_preset = array();
4444 foreach ( $items as $slug => $value ) {
4445 $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value );
4446 }
4447 _wp_array_set( $output, $path, $flattened_preset );
4448 }
4449 }
4450
4451 // If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
4452 // this code unsets them and sets 'appearanceTools' instead.
4453 foreach ( $nodes as $node ) {
4454 $all_opt_ins_are_set = true;
4455 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
4456 $full_path = $node['path'];
4457 foreach ( $opt_in_path as $opt_in_path_item ) {
4458 $full_path[] = $opt_in_path_item;
4459 }
4460 // Use "unset prop" as a marker instead of "null" because
4461 // "null" can be a valid value for some props (e.g. blockGap).
4462 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
4463 if ( 'unset prop' === $opt_in_value ) {
4464 $all_opt_ins_are_set = false;
4465 break;
4466 }
4467 }
4468
4469 if ( $all_opt_ins_are_set ) {
4470 $node_path_with_appearance_tools = $node['path'];
4471 $node_path_with_appearance_tools[] = 'appearanceTools';
4472 _wp_array_set( $output, $node_path_with_appearance_tools, true );
4473 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
4474 $full_path = $node['path'];
4475 foreach ( $opt_in_path as $opt_in_path_item ) {
4476 $full_path[] = $opt_in_path_item;
4477 }
4478 // Use "unset prop" as a marker instead of "null" because
4479 // "null" can be a valid value for some props (e.g. blockGap).
4480 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
4481 if ( true !== $opt_in_value ) {
4482 continue;
4483 }
4484
4485 // The following could be improved to be path independent.
4486 // At the moment it relies on a couple of assumptions:
4487 //
4488 // - all opt-ins having a path of size 2.
4489 // - there's two sources of settings: the top-level and the block-level.
4490 if (
4491 ( 1 === count( $node['path'] ) ) &&
4492 ( 'settings' === $node['path'][0] )
4493 ) {
4494 // Top-level settings.
4495 unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
4496 if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
4497 unset( $output['settings'][ $opt_in_path[0] ] );
4498 }
4499 } elseif (
4500 ( 3 === count( $node['path'] ) ) &&
4501 ( 'settings' === $node['path'][0] ) &&
4502 ( 'blocks' === $node['path'][1] )
4503 ) {
4504 // Block-level settings.
4505 $block_name = $node['path'][2];
4506 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
4507 if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
4508 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
4509 }
4510 }
4511 }
4512 }
4513 }
4514
4515 wp_recursive_ksort( $output );
4516
4517 return $output;
4518 }
4519
4520 /**
4521 * Sets the spacingSizes array based on the spacingScale values from theme.json.
4522 *
4523 * @since 6.1.0
4524 * @deprecated 6.6.0 No longer used as the spacingSizes are automatically
4525 * generated in the constructor and merge methods instead
4526 * of manually after instantiation.
4527 *
4528 * @return null|void
4529 */
4530 public function set_spacing_sizes() {
4531 _deprecated_function( __METHOD__, '6.6.0' );
4532
4533 $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale']['default'] ?? array();
4534
4535 // Gutenberg didn't have the 1st isset check.
4536 if ( ! isset( $spacing_scale['steps'] )
4537 || ! is_numeric( $spacing_scale['steps'] )
4538 || ! isset( $spacing_scale['mediumStep'] )
4539 || ! isset( $spacing_scale['unit'] )
4540 || ! isset( $spacing_scale['operator'] )
4541 || ! isset( $spacing_scale['increment'] )
4542 || ! isset( $spacing_scale['steps'] )
4543 || ! is_numeric( $spacing_scale['increment'] )
4544 || ! is_numeric( $spacing_scale['mediumStep'] )
4545 || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) {
4546 if ( ! empty( $spacing_scale ) ) {
4547 trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid', 'gutenberg' ), E_USER_NOTICE );
4548 }
4549 return null;
4550 }
4551
4552 // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0.
4553 if ( 0 === $spacing_scale['steps'] ) {
4554 return null;
4555 }
4556
4557 $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
4558
4559 // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes.
4560 if ( $spacing_scale['steps'] <= 7 ) {
4561 for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
4562 $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
4563 }
4564 }
4565
4566 _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
4567 }
4568
4569 /**
4570 * Merges two sets of spacing size presets.
4571 *
4572 * @since 6.6.0
4573 *
4574 * @param array $base The base set of spacing sizes.
4575 * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values.
4576 * @return array The merged set of spacing sizes.
4577 */
4578 private static function merge_spacing_sizes( $base, $incoming ) {
4579 // Preserve the order if there are no base (spacingScale) values.
4580 if ( empty( $base ) ) {
4581 return $incoming;
4582 }
4583 $merged = array();
4584 foreach ( $base as $item ) {
4585 $merged[ $item['slug'] ] = $item;
4586 }
4587 foreach ( $incoming as $item ) {
4588 $merged[ $item['slug'] ] = $item;
4589 }
4590 ksort( $merged, SORT_NUMERIC );
4591 return array_values( $merged );
4592 }
4593
4594 /**
4595 * Generates a set of spacing sizes by starting with a medium size and
4596 * applying an operator with an increment value to generate the rest of the
4597 * sizes outward from the medium size. The medium slug is '50' with the rest
4598 * of the slugs being 10 apart. The generated names use t-shirt sizing.
4599 *
4600 * Example:
4601 *
4602 * $spacing_scale = array(
4603 * 'steps' => 4,
4604 * 'mediumStep' => 16,
4605 * 'unit' => 'px',
4606 * 'operator' => '+',
4607 * 'increment' => 2,
4608 * );
4609 * $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
4610 * // -> array(
4611 * // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ),
4612 * // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ),
4613 * // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ),
4614 * // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ),
4615 * // )
4616 *
4617 * @since 6.6.0
4618 *
4619 * @param array $spacing_scale {
4620 * The spacing scale values. All are required.
4621 *
4622 * @type int $steps The number of steps in the scale. (up to 10 steps are supported.)
4623 * @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.)
4624 * @type string $unit The CSS unit to use for the sizes.
4625 * @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'.
4626 * @type float $increment The value used with the operator to generate the other sizes.
4627 * }
4628 * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid.
4629 */
4630 private static function compute_spacing_sizes( $spacing_scale ) {
4631 /*
4632 * This condition is intentionally missing some checks on ranges for the values in order to
4633 * keep backwards compatibility with the previous implementation.
4634 */
4635 if (
4636 ! isset( $spacing_scale['steps'] ) ||
4637 ! is_numeric( $spacing_scale['steps'] ) ||
4638 0 === $spacing_scale['steps'] ||
4639 ! isset( $spacing_scale['mediumStep'] ) ||
4640 ! is_numeric( $spacing_scale['mediumStep'] ) ||
4641 ! isset( $spacing_scale['unit'] ) ||
4642 ! isset( $spacing_scale['operator'] ) ||
4643 ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ||
4644 ! isset( $spacing_scale['increment'] ) ||
4645 ! is_numeric( $spacing_scale['increment'] )
4646 ) {
4647 return array();
4648 }
4649
4650 $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
4651 $current_step = $spacing_scale['mediumStep'];
4652 $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
4653 $x_small_count = null;
4654 $below_sizes = array();
4655 $slug = 40;
4656 $remainder = 0;
4657
4658 for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) {
4659 if ( '+' === $spacing_scale['operator'] ) {
4660 $current_step -= $spacing_scale['increment'];
4661 } elseif ( $spacing_scale['increment'] > 1 ) {
4662 $current_step /= $spacing_scale['increment'];
4663 } else {
4664 $current_step *= $spacing_scale['increment'];
4665 }
4666
4667 if ( $current_step <= 0 ) {
4668 $remainder = $below_midpoint_count;
4669 break;
4670 }
4671
4672 $below_sizes[] = array(
4673 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */
4674 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), (string) $x_small_count ),
4675 'slug' => (string) $slug,
4676 'size' => round( $current_step, 2 ) . $unit,
4677 );
4678
4679 if ( $below_midpoint_count === $steps_mid_point - 2 ) {
4680 $x_small_count = 2;
4681 }
4682
4683 if ( $below_midpoint_count < $steps_mid_point - 2 ) {
4684 ++$x_small_count;
4685 }
4686
4687 $slug -= 10;
4688 }
4689
4690 $below_sizes = array_reverse( $below_sizes );
4691
4692 $below_sizes[] = array(
4693 'name' => __( 'Medium', 'gutenberg' ),
4694 'slug' => '50',
4695 'size' => $spacing_scale['mediumStep'] . $unit,
4696 );
4697
4698 $current_step = $spacing_scale['mediumStep'];
4699 $x_large_count = null;
4700 $above_sizes = array();
4701 $slug = 60;
4702 $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder;
4703
4704 for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) {
4705 $current_step = '+' === $spacing_scale['operator']
4706 ? $current_step + $spacing_scale['increment']
4707 : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] );
4708
4709 $above_sizes[] = array(
4710 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */
4711 'name' => 0 === $above_midpoint_count ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), (string) $x_large_count ),
4712 'slug' => (string) $slug,
4713 'size' => round( $current_step, 2 ) . $unit,
4714 );
4715
4716 if ( 1 === $above_midpoint_count ) {
4717 $x_large_count = 2;
4718 }
4719
4720 if ( $above_midpoint_count > 1 ) {
4721 ++$x_large_count;
4722 }
4723
4724 $slug += 10;
4725 }
4726
4727 $spacing_sizes = $below_sizes;
4728 foreach ( $above_sizes as $above_sizes_item ) {
4729 $spacing_sizes[] = $above_sizes_item;
4730 }
4731
4732 return $spacing_sizes;
4733 }
4734
4735 /**
4736 * Returns the selectors metadata for a block.
4737 *
4738 * @param object $block_type The block type.
4739 * @param string $root_selector The block's root selector.
4740 *
4741 * @return object The custom selectors set by the block.
4742 */
4743 protected static function get_block_selectors( $block_type, $root_selector ) {
4744 if ( ! empty( $block_type->selectors ) ) {
4745 return $block_type->selectors;
4746 }
4747
4748 $selectors = array( 'root' => $root_selector );
4749 foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) {
4750 $feature_selector = wp_get_block_css_selector( $block_type, $key );
4751 if ( null !== $feature_selector ) {
4752 $selectors[ $feature ] = array( 'root' => $feature_selector );
4753 }
4754 }
4755
4756 return $selectors;
4757 }
4758
4759 /**
4760 * Generates all the element selectors for a block.
4761 *
4762 * @param string $root_selector The block's root CSS selector.
4763 * @return array The block's element selectors.
4764 */
4765 protected static function get_block_element_selectors( $root_selector ) {
4766 // Assign defaults, then override those that the block sets by itself.
4767 // If the block selector is compounded, will append the element to each
4768 // individual block selector.
4769 $block_selectors = explode( ',', $root_selector );
4770 $element_selectors = array();
4771
4772 foreach ( static::ELEMENTS as $el_name => $el_selector ) {
4773 $element_selector = array();
4774 foreach ( $block_selectors as $selector ) {
4775 if ( $selector === $el_selector ) {
4776 $element_selector = array( $el_selector );
4777 break;
4778 }
4779 $element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' );
4780 }
4781 $element_selectors[ $el_name ] = implode( ',', $element_selector );
4782 }
4783
4784 return $element_selectors;
4785 }
4786
4787 /**
4788 * Generates style declarations for a node's features e.g. color, border,
4789 * typography etc, that have custom selectors in their related block's
4790 * metadata.
4791 *
4792 * @param object $metadata The related block metadata containing selectors.
4793 * @param object $node A merged theme.json node for block or variation.
4794 *
4795 * @return array The style declarations for the node's features with custom
4796 * selectors.
4797 */
4798 protected function get_feature_declarations_for_node( $metadata, &$node ) {
4799 $declarations = array();
4800
4801 if ( ! isset( $metadata['selectors'] ) ) {
4802 return $declarations;
4803 }
4804
4805 $settings = $this->theme_json['settings'] ?? null;
4806
4807 foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
4808 // Skip if this is the block's root selector, the custom CSS
4809 // selector, or the block doesn't have any styles for the feature.
4810 if ( 'root' === $feature || 'css' === $feature || empty( $node[ $feature ] ) ) {
4811 continue;
4812 }
4813
4814 if ( is_array( $feature_selectors ) ) {
4815 foreach ( $feature_selectors as $subfeature => $subfeature_selector ) {
4816 if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) {
4817 continue;
4818 }
4819
4820 // Create temporary node containing only the subfeature data
4821 // to leverage existing `compute_style_properties` function.
4822 $subfeature_node = array(
4823 $feature => array(
4824 $subfeature => $node[ $feature ][ $subfeature ],
4825 ),
4826 );
4827
4828 // Generate style declarations.
4829 $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json );
4830
4831 // Merge subfeature declarations into feature declarations.
4832 if ( isset( $declarations[ $subfeature_selector ] ) ) {
4833 foreach ( $new_declarations as $new_declaration ) {
4834 $declarations[ $subfeature_selector ][] = $new_declaration;
4835 }
4836 } else {
4837 $declarations[ $subfeature_selector ] = $new_declarations;
4838 }
4839
4840 // Remove the subfeature from the block's node now its
4841 // styles will be included under its own selector not the
4842 // block's.
4843 unset( $node[ $feature ][ $subfeature ] );
4844 }
4845 }
4846
4847 // Now subfeatures have been processed and removed we can process
4848 // feature root selector or simple string selector.
4849 if (
4850 is_string( $feature_selectors ) ||
4851 ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] )
4852 ) {
4853 $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root'];
4854
4855 // Create temporary node containing only the feature data
4856 // to leverage existing `compute_style_properties` function.
4857 $feature_node = array( $feature => $node[ $feature ] );
4858
4859 // Generate the style declarations.
4860 $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json );
4861
4862 // Merge new declarations with any that already exist for
4863 // the feature selector. This may occur when multiple block
4864 // support features use the same custom selector.
4865 if ( isset( $declarations[ $feature_selector ] ) ) {
4866 foreach ( $new_declarations as $new_declaration ) {
4867 $declarations[ $feature_selector ][] = $new_declaration;
4868 }
4869 } else {
4870 $declarations[ $feature_selector ] = $new_declarations;
4871 }
4872
4873 // Remove the feature from the block's node now its styles
4874 // will be included under its own selector not the block's.
4875 unset( $node[ $feature ] );
4876 }
4877 }
4878
4879 return $declarations;
4880 }
4881
4882 /**
4883 * This is used to convert the internal representation of variables to the CSS representation.
4884 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
4885 *
4886 * @since 6.3.0
4887 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
4888 * @return string The converted variable.
4889 */
4890 private static function convert_custom_properties( $value ) {
4891 $prefix = 'var:';
4892 $prefix_len = strlen( $prefix );
4893 $token_in = '|';
4894 $token_out = '--';
4895 if ( 0 === strpos( $value, $prefix ) ) {
4896 $unwrapped_name = str_replace(
4897 $token_in,
4898 $token_out,
4899 substr( $value, $prefix_len )
4900 );
4901 $value = "var(--wp--$unwrapped_name)";
4902 }
4903
4904 return $value;
4905 }
4906
4907 /**
4908 * Given a tree, converts the internal representation of variables to the CSS representation.
4909 * It is recursive and modifies the input in-place.
4910 *
4911 * @since 6.3.0
4912 * @param array $tree Input to process.
4913 * @return array The modified $tree.
4914 */
4915 private static function resolve_custom_css_format( $tree ) {
4916 $prefix = 'var:';
4917
4918 foreach ( $tree as $key => $data ) {
4919 if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
4920 $tree[ $key ] = self::convert_custom_properties( $data );
4921 } elseif ( is_array( $data ) ) {
4922 $tree[ $key ] = self::resolve_custom_css_format( $data );
4923 }
4924 }
4925
4926 return $tree;
4927 }
4928
4929 /**
4930 * Replaces CSS variables with their values in place.
4931 *
4932 * @since 6.3.0
4933 * @since 6.6.0 Check for empty style before processing.
4934 *
4935 * @param array $styles CSS declarations to convert.
4936 * @param array $values key => value pairs to use for replacement.
4937 * @return array
4938 */
4939 private static function convert_variables_to_value( $styles, $values ) {
4940 foreach ( $styles as $key => $style ) {
4941 if ( empty( $style ) ) {
4942 continue;
4943 }
4944
4945 if ( is_array( $style ) ) {
4946 $styles[ $key ] = self::convert_variables_to_value( $style, $values );
4947 continue;
4948 }
4949
4950 if ( 0 <= strpos( $style, 'var(' ) ) {
4951 // find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group.
4952
4953 $has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts );
4954
4955 if ( $has_matches ) {
4956 $resolved_style = $styles[ $key ];
4957 foreach ( $var_parts[1] as $index => $var_part ) {
4958 $key_in_values = 'var(' . $var_part . ')';
4959 $rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan).
4960 $fallback = $var_parts[2][ $index ]; // the fallback value.
4961 $resolved_style = str_replace(
4962 array(
4963 $rule_to_replace,
4964 $fallback,
4965 ),
4966 array(
4967 $values[ $key_in_values ] ?? $rule_to_replace,
4968 $values[ $fallback ] ?? $fallback,
4969 ),
4970 $resolved_style
4971 );
4972 }
4973 $styles[ $key ] = $resolved_style;
4974 }
4975 }
4976 }
4977
4978 return $styles;
4979 }
4980
4981 /**
4982 * Resolves the values of CSS variables in the given styles.
4983 *
4984 * @since 6.3.0
4985 * @param WP_Theme_JSON_Gutenberg $theme_json The theme json resolver.
4986 *
4987 * @return WP_Theme_JSON_Gutenberg The $theme_json with resolved variables.
4988 */
4989 public static function resolve_variables( $theme_json ) {
4990 $settings = $theme_json->get_settings();
4991 $styles = $theme_json->get_raw_data()['styles'];
4992 $preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS );
4993 $theme_vars = static::compute_theme_vars( $settings );
4994 $vars = array_reduce(
4995 array_merge( $preset_vars, $theme_vars ),
4996 function ( $carry, $item ) {
4997 $name = $item['name'];
4998 $carry[ "var({$name})" ] = $item['value'];
4999 return $carry;
5000 },
5001 array()
5002 );
5003
5004 $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars );
5005 return $theme_json;
5006 }
5007
5008 /**
5009 * Generates a selector for a block style variation.
5010 *
5011 * @param string $variation_name Name of the block style variation.
5012 * @param string $block_selector CSS selector for the block.
5013 *
5014 * @return string Block selector with block style variation selector added to it.
5015 */
5016 protected static function get_block_style_variation_selector( $variation_name, $block_selector ) {
5017 $variation_class = ".is-style-$variation_name";
5018
5019 if ( ! $block_selector ) {
5020 return $variation_class;
5021 }
5022
5023 $limit = 1;
5024 $selector_parts = explode( ',', $block_selector );
5025 $result = array();
5026
5027 foreach ( $selector_parts as $part ) {
5028 $result[] = preg_replace_callback(
5029 '/((?::\([^)]+\))?\s*)([^\s:]+)/',
5030 function ( $matches ) use ( $variation_class ) {
5031 return $matches[1] . $matches[2] . $variation_class;
5032 },
5033 $part,
5034 $limit
5035 );
5036 }
5037
5038 return implode( ',', $result );
5039 }
5040
5041 /**
5042 * Collects valid block style variations keyed by block type.
5043 *
5044 * @since 6.6.0
5045 * @since 6.8.0 Added the `$blocks_metadata` parameter.
5046 *
5047 * @param array $blocks_metadata Optional. List of metadata per block. Default is the metadata for all blocks.
5048 * @return array Valid block style variations by block type.
5049 */
5050 protected static function get_valid_block_style_variations( $blocks_metadata = array() ) {
5051 $valid_variations = array();
5052 $blocks_metadata = empty( $blocks_metadata ) ? static::get_blocks_metadata() : $blocks_metadata;
5053 foreach ( $blocks_metadata as $block_name => $block_meta ) {
5054 if ( ! isset( $block_meta['styleVariations'] ) ) {
5055 continue;
5056 }
5057 $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
5058 }
5059
5060 return $valid_variations;
5061 }
5062
5063 /**
5064 * Extracts the block name from the block metadata path.
5065 *
5066 * @since 7.0
5067 *
5068 * @param array $block_metadata Block metadata.
5069 * @return string|null The block name or null if not found.
5070 */
5071 private static function get_block_name_from_metadata_path( $block_metadata ) {
5072 if ( isset( $block_metadata['path'] ) ) {
5073 return $block_metadata['path'][2];
5074 }
5075 }
5076 }
5077