PluginProbe
Gutenberg / 22.3.0
Gutenberg v22.3.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 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 22.3.0, at lib/class-wp-theme-json-gutenberg.php

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