PluginProbe
Gutenberg / 23.0.1
Gutenberg v23.0.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.0.1, at lib/class-wp-theme-json-gutenberg.php

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