PluginProbe
Gutenberg / 23.3.2
Gutenberg v23.3.2
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / class-wp-theme-json-gutenberg.php

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

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