PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 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 All 404 releases
gutenberg / lib / class-wp-theme-json-gutenberg.php

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

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