PluginProbe
Gutenberg / 23.5.1
Gutenberg v23.5.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.5.1, at lib/class-wp-theme-json-gutenberg.php

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