PluginProbe ʕ •ᴥ•ʔ
Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets / 4.0.5
Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets v4.0.5
4.2.5 4.2.4 trunk 3.7.10 3.7.11 3.7.12 3.7.13 3.7.14 3.7.2 3.7.5 3.7.6 3.7.7 3.7.8 3.7.9 3.8 3.8.1 3.8.10 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.8.8 3.8.9 3.8.9.1 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.5.1 4.0.6 4.0.6.1 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2.0 4.2.1 4.2.2 4.2.3
widget-options / includes / widgets / gutenberg / lib / class-wp-theme-json-gutenberg.php
widget-options / includes / widgets / gutenberg / lib Last commit date
block-supports 2 years ago compat 2 years ago experimental 2 years ago README.md 2 years ago block-editor-settings.php 2 years ago blocks.php 2 years ago class-wp-duotone-gutenberg.php 2 years ago class-wp-theme-json-data-gutenberg.php 2 years ago class-wp-theme-json-gutenberg.php 2 years ago class-wp-theme-json-resolver-gutenberg.php 2 years ago client-assets.php 2 years ago demo.php 2 years ago experiments-page.php 2 years ago global-styles-and-settings.php 2 years ago init.php 2 years ago load.php 2 years ago script-loader.php 2 years ago theme-i18n.json 2 years ago theme.json 2 years ago upgrade.php 2 years ago
class-wp-theme-json-gutenberg.php
3615 lines
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 styles.
42 *
43 * @since 5.8.0
44 * @var string
45 */
46 const ROOT_BLOCK_SELECTOR = 'body';
47
48 /**
49 * The sources of data this object can represent.
50 *
51 * @since 5.8.0
52 * @since 6.1.0 Added 'blocks'.
53 * @var string[]
54 */
55 const VALID_ORIGINS = array(
56 'default',
57 'blocks',
58 'theme',
59 'custom',
60 );
61
62 /**
63 * Presets are a set of values that serve
64 * to bootstrap some styles: colors, font sizes, etc.
65 *
66 * They are a unkeyed array of values such as:
67 *
68 * ```php
69 * array(
70 * array(
71 * 'slug' => 'unique-name-within-the-set',
72 * 'name' => 'Name for the UI',
73 * <value_key> => 'value'
74 * ),
75 * )
76 * ```
77 *
78 * This contains the necessary metadata to process them:
79 *
80 * - path => Where to find the preset within the settings section.
81 * - prevent_override => Disables override of default presets by theme presets.
82 * The relationship between whether to override the defaults
83 * and whether the defaults are enabled is inverse:
84 * - If defaults are enabled => theme presets should not be overridden
85 * - If defaults are disabled => theme presets should be overridden
86 * For example, a theme sets defaultPalette to false,
87 * making the default palette hidden from the user.
88 * In that case, we want all the theme presets to be present,
89 * so they should override the defaults by setting this false.
90 * - use_default_names => whether to use the default names
91 * - value_key => the key that represents the value
92 * - value_func => optionally, instead of value_key, a function to generate
93 * the value that takes a preset as an argument
94 * (either value_key or value_func should be present)
95 * - css_vars => template string to use in generating the CSS Custom Property.
96 * Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
97 * substituting the $slug for the slug's value for each preset value.
98 * - classes => array containing a structure with the classes to
99 * generate for the presets, where for each array item
100 * the key is the class name and the value the property name.
101 * The "$slug" substring will be replaced by the slug of each preset.
102 * For example:
103 * 'classes' => array(
104 * '.has-$slug-color' => 'color',
105 * '.has-$slug-background-color' => 'background-color',
106 * '.has-$slug-border-color' => 'border-color',
107 * )
108 * - properties => array of CSS properties to be used by kses to
109 * validate the content of each preset
110 * by means of the remove_insecure_properties method.
111 *
112 * @since 5.8.0
113 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
114 * `use_default_names` preset key, and simplified the metadata structure.
115 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
116 * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
117 * @since 6.2.0 Added 'shadow' presets.
118 * @var array
119 */
120 const PRESETS_METADATA = array(
121 array(
122 'path' => array( 'color', 'palette' ),
123 'prevent_override' => array( 'color', 'defaultPalette' ),
124 'use_default_names' => false,
125 'value_key' => 'color',
126 'css_vars' => '--wp--preset--color--$slug',
127 'classes' => array(
128 '.has-$slug-color' => 'color',
129 '.has-$slug-background-color' => 'background-color',
130 '.has-$slug-border-color' => 'border-color',
131 ),
132 'properties' => array( 'color', 'background-color', 'border-color' ),
133 ),
134 array(
135 'path' => array( 'color', 'gradients' ),
136 'prevent_override' => array( 'color', 'defaultGradients' ),
137 'use_default_names' => false,
138 'value_key' => 'gradient',
139 'css_vars' => '--wp--preset--gradient--$slug',
140 'classes' => array( '.has-$slug-gradient-background' => 'background' ),
141 'properties' => array( 'background' ),
142 ),
143 array(
144 'path' => array( 'color', 'duotone' ),
145 'prevent_override' => array( 'color', 'defaultDuotone' ),
146 'use_default_names' => false,
147 'value_func' => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone-gutenberg.php.
148 'css_vars' => null,
149 'classes' => array(),
150 'properties' => array( 'filter' ),
151 ),
152 array(
153 'path' => array( 'typography', 'fontSizes' ),
154 'prevent_override' => false,
155 'use_default_names' => true,
156 'value_func' => 'gutenberg_get_typography_font_size_value',
157 'css_vars' => '--wp--preset--font-size--$slug',
158 'classes' => array( '.has-$slug-font-size' => 'font-size' ),
159 'properties' => array( 'font-size' ),
160 ),
161 array(
162 'path' => array( 'typography', 'fontFamilies' ),
163 'prevent_override' => false,
164 'use_default_names' => false,
165 'value_key' => 'fontFamily',
166 'css_vars' => '--wp--preset--font-family--$slug',
167 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
168 'properties' => array( 'font-family' ),
169 ),
170 array(
171 'path' => array( 'spacing', 'spacingSizes' ),
172 'prevent_override' => false,
173 'use_default_names' => true,
174 'value_key' => 'size',
175 'css_vars' => '--wp--preset--spacing--$slug',
176 'classes' => array(),
177 'properties' => array( 'padding', 'margin' ),
178 ),
179 array(
180 'path' => array( 'shadow', 'presets' ),
181 'prevent_override' => array( 'shadow', 'defaultPresets' ),
182 'use_default_names' => false,
183 'value_key' => 'shadow',
184 'css_vars' => '--wp--preset--shadow--$slug',
185 'classes' => array(),
186 'properties' => array( 'box-shadow' ),
187 ),
188 );
189
190 /**
191 * Metadata for style properties.
192 *
193 * Each element is a direct mapping from the CSS property name to the
194 * path to the value in theme.json & block attributes.
195 *
196 * @since 5.8.0
197 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
198 * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
199 * `text-decoration`, `text-transform`, and `filter` properties,
200 * simplified the metadata structure.
201 * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`,
202 * `--wp--style--root--padding-*`, and `box-shadow` properties,
203 * removed the `--wp--style--block-gap` property.
204 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
205 *
206 * @var array
207 */
208 const PROPERTIES_METADATA = array(
209 'background' => array( 'color', 'gradient' ),
210 'background-color' => array( 'color', 'background' ),
211 'border-radius' => array( 'border', 'radius' ),
212 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ),
213 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ),
214 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ),
215 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
216 'border-color' => array( 'border', 'color' ),
217 'border-width' => array( 'border', 'width' ),
218 'border-style' => array( 'border', 'style' ),
219 'border-top-color' => array( 'border', 'top', 'color' ),
220 'border-top-width' => array( 'border', 'top', 'width' ),
221 'border-top-style' => array( 'border', 'top', 'style' ),
222 'border-right-color' => array( 'border', 'right', 'color' ),
223 'border-right-width' => array( 'border', 'right', 'width' ),
224 'border-right-style' => array( 'border', 'right', 'style' ),
225 'border-bottom-color' => array( 'border', 'bottom', 'color' ),
226 'border-bottom-width' => array( 'border', 'bottom', 'width' ),
227 'border-bottom-style' => array( 'border', 'bottom', 'style' ),
228 'border-left-color' => array( 'border', 'left', 'color' ),
229 'border-left-width' => array( 'border', 'left', 'width' ),
230 'border-left-style' => array( 'border', 'left', 'style' ),
231 'color' => array( 'color', 'text' ),
232 'column-count' => array( 'typography', 'textColumns' ),
233 'font-family' => array( 'typography', 'fontFamily' ),
234 'font-size' => array( 'typography', 'fontSize' ),
235 'font-style' => array( 'typography', 'fontStyle' ),
236 'font-weight' => array( 'typography', 'fontWeight' ),
237 'letter-spacing' => array( 'typography', 'letterSpacing' ),
238 'line-height' => array( 'typography', 'lineHeight' ),
239 'margin' => array( 'spacing', 'margin' ),
240 'margin-top' => array( 'spacing', 'margin', 'top' ),
241 'margin-right' => array( 'spacing', 'margin', 'right' ),
242 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
243 'margin-left' => array( 'spacing', 'margin', 'left' ),
244 'min-height' => array( 'dimensions', 'minHeight' ),
245 'outline-color' => array( 'outline', 'color' ),
246 'outline-offset' => array( 'outline', 'offset' ),
247 'outline-style' => array( 'outline', 'style' ),
248 'outline-width' => array( 'outline', 'width' ),
249 'padding' => array( 'spacing', 'padding' ),
250 'padding-top' => array( 'spacing', 'padding', 'top' ),
251 'padding-right' => array( 'spacing', 'padding', 'right' ),
252 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
253 'padding-left' => array( 'spacing', 'padding', 'left' ),
254 '--wp--style--root--padding' => array( 'spacing', 'padding' ),
255 '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ),
256 '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
257 '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
258 '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
259 'text-decoration' => array( 'typography', 'textDecoration' ),
260 'text-transform' => array( 'typography', 'textTransform' ),
261 'filter' => array( 'filter', 'duotone' ),
262 'box-shadow' => array( 'shadow' ),
263 );
264
265 /**
266 * Indirect metadata for style properties that are not directly output.
267 *
268 * Each element maps from a CSS property name to an array of
269 * paths to the value in theme.json & block attributes.
270 *
271 * Indirect properties are not output directly by `compute_style_properties`,
272 * but are used elsewhere in the processing of global styles. The indirect
273 * property is used to validate whether or not a style value is allowed.
274 *
275 * @since 6.2.0
276 *
277 * @var array
278 */
279 const INDIRECT_PROPERTIES_METADATA = array(
280 'gap' => array(
281 array( 'spacing', 'blockGap' ),
282 ),
283 'column-gap' => array(
284 array( 'spacing', 'blockGap', 'left' ),
285 ),
286 'row-gap' => array(
287 array( 'spacing', 'blockGap', 'top' ),
288 ),
289 'max-width' => array(
290 array( 'layout', 'contentSize' ),
291 array( 'layout', 'wideSize' ),
292 ),
293 );
294
295 /**
296 * Protected style properties.
297 *
298 * These style properties are only rendered if a setting enables it
299 * via a value other than `null`.
300 *
301 * Each element maps the style property to the corresponding theme.json
302 * setting key.
303 *
304 * @since 5.9.0
305 */
306 const PROTECTED_PROPERTIES = array(
307 'spacing.blockGap' => array( 'spacing', 'blockGap' ),
308 );
309
310 /**
311 * The top-level keys a theme.json can have.
312 *
313 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
314 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
315 * added the `customTemplates` and `templateParts` values.
316 * @var string[]
317 */
318 const VALID_TOP_LEVEL_KEYS = array(
319 'customTemplates',
320 'patterns',
321 'settings',
322 'styles',
323 'templateParts',
324 'title',
325 'version',
326 );
327
328 /**
329 * The valid properties under the settings key.
330 *
331 * @since 5.8.0 As `ALLOWED_SETTINGS`.
332 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
333 * added new properties for `border`, `color`, `spacing`,
334 * and `typography`, and renamed others according to the new schema.
335 * @since 6.0.0 Added `color.defaultDuotone`.
336 * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
337 * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets',
338 * `position.fixed` and `position.sticky`.
339 * @var array
340 */
341 const VALID_SETTINGS = array(
342 'appearanceTools' => null,
343 'useRootPaddingAwareAlignments' => null,
344 'border' => array(
345 'color' => null,
346 'radius' => null,
347 'style' => null,
348 'width' => null,
349 ),
350 'color' => array(
351 'background' => null,
352 'custom' => null,
353 'customDuotone' => null,
354 'customGradient' => null,
355 'defaultDuotone' => null,
356 'defaultGradients' => null,
357 'defaultPalette' => null,
358 'duotone' => null,
359 'gradients' => null,
360 'link' => null,
361 'heading' => null,
362 'button' => null,
363 'caption' => null,
364 'palette' => null,
365 'text' => null,
366 ),
367 'custom' => null,
368 'dimensions' => array(
369 'minHeight' => null,
370 ),
371 'layout' => array(
372 'contentSize' => null,
373 'definitions' => null,
374 'wideSize' => null,
375 ),
376 'position' => array(
377 'fixed' => null,
378 'sticky' => null,
379 ),
380 'spacing' => array(
381 'customSpacingSize' => null,
382 'spacingSizes' => null,
383 'spacingScale' => null,
384 'blockGap' => null,
385 'margin' => null,
386 'padding' => null,
387 'units' => null,
388 ),
389 'shadow' => array(
390 'presets' => null,
391 'defaultPresets' => null,
392 ),
393 'typography' => array(
394 'fluid' => null,
395 'customFontSize' => null,
396 'dropCap' => null,
397 'fontFamilies' => null,
398 'fontSizes' => null,
399 'fontStyle' => null,
400 'fontWeight' => null,
401 'letterSpacing' => null,
402 'lineHeight' => null,
403 'textColumns' => null,
404 'textDecoration' => null,
405 'textTransform' => null,
406 ),
407 );
408
409 /**
410 * The valid properties under the styles key.
411 *
412 * @since 5.8.0 As `ALLOWED_STYLES`.
413 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
414 * added new properties for `border`, `filter`, `spacing`,
415 * and `typography`.
416 * @since 6.1.0 Added new side properties for `border`,
417 * added new property `shadow`,
418 * updated `blockGap` to be allowed at any level.
419 * @since 6.2.0 Added `outline`, and `minHeight` properties.
420 *
421 * @var array
422 */
423 const VALID_STYLES = array(
424 'border' => array(
425 'color' => null,
426 'radius' => null,
427 'style' => null,
428 'width' => null,
429 'top' => null,
430 'right' => null,
431 'bottom' => null,
432 'left' => null,
433 ),
434 'color' => array(
435 'background' => null,
436 'gradient' => null,
437 'text' => null,
438 ),
439 'dimensions' => array(
440 'minHeight' => null,
441 ),
442 'filter' => array(
443 'duotone' => null,
444 ),
445 'outline' => array(
446 'color' => null,
447 'offset' => null,
448 'style' => null,
449 'width' => null,
450 ),
451 'shadow' => null,
452 'spacing' => array(
453 'margin' => null,
454 'padding' => null,
455 'blockGap' => null,
456 ),
457 'typography' => array(
458 'fontFamily' => null,
459 'fontSize' => null,
460 'fontStyle' => null,
461 'fontWeight' => null,
462 'letterSpacing' => null,
463 'lineHeight' => null,
464 'textColumns' => null,
465 'textDecoration' => null,
466 'textTransform' => null,
467 ),
468 'css' => null,
469 );
470
471 /**
472 * Defines which pseudo selectors are enabled for which elements.
473 *
474 * The order of the selectors should be: link, any-link, visited, hover, focus, active.
475 * This is to ensure the user action (hover, focus and active) styles have a higher
476 * specificity than the visited styles, which in turn have a higher specificity than
477 * the unvisited styles.
478 *
479 * See https://core.trac.wordpress.org/ticket/56928.
480 * Note: this will affect both top-level and block-level elements.
481 *
482 * @since 6.1.0
483 * @since 6.2.0 Added support for `:link` and `:any-link`.
484 */
485 const VALID_ELEMENT_PSEUDO_SELECTORS = array(
486 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
487 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
488 );
489
490 /**
491 * The valid elements that can be found under styles.
492 *
493 * @since 5.8.0
494 * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
495 * @var string[]
496 */
497 const ELEMENTS = array(
498 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
499 'heading' => 'h1, h2, h3, h4, h5, h6',
500 'h1' => 'h1',
501 'h2' => 'h2',
502 'h3' => 'h3',
503 'h4' => 'h4',
504 'h5' => 'h5',
505 'h6' => 'h6',
506 // We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
507 'button' => '.wp-element-button, .wp-block-button__link',
508 // The block classes are necessary to target older content that won't use the new class names.
509 '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',
510 'cite' => 'cite',
511 );
512
513 const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
514 'button' => 'wp-element-button',
515 'caption' => 'wp-element-caption',
516 );
517
518 /**
519 * List of block support features that can have their related styles
520 * generated under their own feature level selector rather than the block's.
521 *
522 * @since 6.1.0
523 * @var string[]
524 */
525 const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
526 '__experimentalBorder' => 'border',
527 'color' => 'color',
528 'spacing' => 'spacing',
529 'typography' => 'typography',
530 );
531
532 /**
533 * Returns a class name by an element name.
534 *
535 * @since 6.1.0
536 *
537 * @param string $element The name of the element.
538 * @return string The name of the class.
539 */
540 public static function get_element_class_name( $element ) {
541 $class_name = '';
542
543 // TODO: Replace array_key_exists() with isset() check once WordPress drops
544 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
545 if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) {
546 $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
547 }
548
549 return $class_name;
550 }
551
552 /**
553 * Options that settings.appearanceTools enables.
554 *
555 * @since 6.0.0
556 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
557 * @var array
558 */
559 const APPEARANCE_TOOLS_OPT_INS = array(
560 array( 'border', 'color' ),
561 array( 'border', 'radius' ),
562 array( 'border', 'style' ),
563 array( 'border', 'width' ),
564 array( 'color', 'link' ),
565 array( 'color', 'heading' ),
566 array( 'color', 'button' ),
567 array( 'color', 'caption' ),
568 array( 'dimensions', 'minHeight' ),
569 // BEGIN EXPERIMENTAL.
570 // Allow `position.fixed` to be opted-in by default.
571 // Sticky position support was backported to WordPress 6.2 in https://core.trac.wordpress.org/ticket/57618.
572 // While `fixed` was included as a valid setting, exposing it by default is still experimental.
573 array( 'position', 'fixed' ),
574 // END EXPERIMENTAL.
575 array( 'position', 'sticky' ),
576 array( 'spacing', 'blockGap' ),
577 array( 'spacing', 'margin' ),
578 array( 'spacing', 'padding' ),
579 array( 'typography', 'lineHeight' ),
580 );
581
582 /**
583 * The latest version of the schema in use.
584 *
585 * @since 5.8.0
586 * @since 5.9.0 Changed value from 1 to 2.
587 * @var int
588 */
589 const LATEST_SCHEMA = 2;
590
591 /**
592 * Constructor.
593 *
594 * @since 5.8.0
595 *
596 * @param array $theme_json A structure that follows the theme.json schema.
597 * @param string $origin Optional. What source of data this object represents.
598 * One of 'default', 'theme', or 'custom'. Default 'theme'.
599 */
600 public function __construct( $theme_json = array(), $origin = 'theme' ) {
601 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
602 $origin = 'theme';
603 }
604
605 $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
606 $registry = WP_Block_Type_Registry::get_instance();
607 $valid_block_names = array_keys( $registry->get_all_registered() );
608 $valid_element_names = array_keys( static::ELEMENTS );
609 $valid_variations = array();
610 foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) {
611 if ( ! isset( $block_meta['styleVariations'] ) ) {
612 continue;
613 }
614 $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
615 }
616 $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
617 $this->theme_json = static::maybe_opt_in_into_settings( $theme_json );
618
619 // Internally, presets are keyed by origin.
620 $nodes = static::get_setting_nodes( $this->theme_json );
621 foreach ( $nodes as $node ) {
622 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
623 $path = $node['path'];
624 foreach ( $preset_metadata['path'] as $subpath ) {
625 $path[] = $subpath;
626 }
627 $preset = _wp_array_get( $this->theme_json, $path, null );
628 if ( null !== $preset ) {
629 // If the preset is not already keyed by origin.
630 if ( isset( $preset[0] ) || empty( $preset ) ) {
631 _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
632 }
633 }
634 }
635 }
636 }
637
638 /**
639 * Enables some opt-in settings if theme declared support.
640 *
641 * @since 5.9.0
642 *
643 * @param array $theme_json A theme.json structure to modify.
644 * @return array The modified theme.json structure.
645 */
646 protected static function maybe_opt_in_into_settings( $theme_json ) {
647 $new_theme_json = $theme_json;
648
649 if (
650 isset( $new_theme_json['settings']['appearanceTools'] ) &&
651 true === $new_theme_json['settings']['appearanceTools']
652 ) {
653 static::do_opt_in_into_settings( $new_theme_json['settings'] );
654 }
655
656 if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
657 foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
658 if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
659 static::do_opt_in_into_settings( $block );
660 }
661 }
662 }
663
664 return $new_theme_json;
665 }
666
667 /**
668 * Enables some settings.
669 *
670 * @since 5.9.0
671 *
672 * @param array $context The context to which the settings belong.
673 */
674 protected static function do_opt_in_into_settings( &$context ) {
675 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
676 // Use "unset prop" as a marker instead of "null" because
677 // "null" can be a valid value for some props (e.g. blockGap).
678 if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
679 _wp_array_set( $context, $path, true );
680 }
681 }
682
683 unset( $context['appearanceTools'] );
684 }
685
686 /**
687 * Sanitizes the input according to the schemas.
688 *
689 * @since 5.8.0
690 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
691 *
692 * @param array $input Structure to sanitize.
693 * @param array $valid_block_names List of valid block names.
694 * @param array $valid_element_names List of valid element names.
695 * @param array $valid_variations List of valid variations per block.
696 * @return array The sanitized output.
697 */
698 protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) {
699
700 $output = array();
701
702 if ( ! is_array( $input ) ) {
703 return $output;
704 }
705
706 // Preserve only the top most level keys.
707 $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
708
709 /*
710 * Remove any rules that are annotated as "top" in VALID_STYLES constant.
711 * Some styles are only meant to be available at the top-level (e.g.: blockGap),
712 * hence, the schema for blocks & elements should not have them.
713 */
714 $styles_non_top_level = static::VALID_STYLES;
715 foreach ( array_keys( $styles_non_top_level ) as $section ) {
716 // array_key_exists() needs to be used instead of isset() because the value can be null.
717 if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
718 foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
719 if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
720 unset( $styles_non_top_level[ $section ][ $prop ] );
721 }
722 }
723 }
724 }
725
726 // Build the schema based on valid block & element names.
727 $schema = array();
728 $schema_styles_elements = array();
729
730 /*
731 * Set allowed element pseudo selectors based on per element allow list.
732 * Target data structure in schema:
733 * e.g.
734 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
735 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
736 */
737 foreach ( $valid_element_names as $element ) {
738 $schema_styles_elements[ $element ] = $styles_non_top_level;
739
740 // TODO: Replace array_key_exists() with isset() check once WordPress drops
741 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
742 if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
743 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
744 $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
745 }
746 }
747 }
748
749 $schema_styles_blocks = array();
750 $schema_settings_blocks = array();
751 foreach ( $valid_block_names as $block ) {
752 // Build the schema for each block style variation.
753 $style_variation_names = array();
754 if (
755 ! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
756 is_array( $input['styles']['blocks'][ $block ]['variations'] ) &&
757 isset( $valid_variations[ $block ] )
758 ) {
759 $style_variation_names = array_intersect(
760 array_keys( $input['styles']['blocks'][ $block ]['variations'] ),
761 $valid_variations[ $block ]
762 );
763 }
764
765 $schema_styles_variations = array();
766 if ( ! empty( $style_variation_names ) ) {
767 $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level );
768 }
769
770 $schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
771 $schema_styles_blocks[ $block ] = $styles_non_top_level;
772 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
773 $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
774 }
775
776 $schema['styles'] = static::VALID_STYLES;
777 $schema['styles']['blocks'] = $schema_styles_blocks;
778 $schema['styles']['elements'] = $schema_styles_elements;
779 $schema['settings'] = static::VALID_SETTINGS;
780 $schema['settings']['blocks'] = $schema_settings_blocks;
781
782 // Remove anything that's not present in the schema.
783 foreach ( array( 'styles', 'settings' ) as $subtree ) {
784 if ( ! isset( $input[ $subtree ] ) ) {
785 continue;
786 }
787
788 if ( ! is_array( $input[ $subtree ] ) ) {
789 unset( $output[ $subtree ] );
790 continue;
791 }
792
793 $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
794
795 if ( empty( $result ) ) {
796 unset( $output[ $subtree ] );
797 } else {
798 $output[ $subtree ] = static::resolve_custom_css_format( $result );
799 }
800 }
801
802 return $output;
803 }
804
805 /**
806 * Appends a sub-selector to an existing one.
807 *
808 * Given the compounded $selector "h1, h2, h3"
809 * and the $to_append selector ".some-class" the result will be
810 * "h1.some-class, h2.some-class, h3.some-class".
811 *
812 * @since 5.8.0
813 * @since 6.1.0 Added append position.
814 *
815 * @param string $selector Original selector.
816 * @param string $to_append Selector to append.
817 * @param string $position A position sub-selector should be appended. Default 'right'.
818 * @return string The new selector.
819 */
820 protected static function append_to_selector( $selector, $to_append, $position = 'right' ) {
821 if ( ! str_contains( $selector, ',' ) ) {
822 return 'right' === $position ? $selector . $to_append : $to_append . $selector;
823 }
824 $new_selectors = array();
825 $selectors = explode( ',', $selector );
826 foreach ( $selectors as $sel ) {
827 $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel;
828 }
829 return implode( ',', $new_selectors );
830 }
831
832 /**
833 * Returns the metadata for each block.
834 *
835 * Example:
836 *
837 * {
838 * 'core/paragraph': {
839 * 'selector': 'p',
840 * 'elements': {
841 * 'link' => 'link selector',
842 * 'etc' => 'element selector'
843 * }
844 * },
845 * 'core/heading': {
846 * 'selector': 'h1',
847 * 'elements': {}
848 * },
849 * 'core/image': {
850 * 'selector': '.wp-block-image',
851 * 'duotone': 'img',
852 * 'elements': {}
853 * }
854 * }
855 *
856 * @since 5.8.0
857 * @since 5.9.0 Added `duotone` key with CSS selector.
858 * @since 6.1.0 Added `features` key with block support feature level selectors.
859 *
860 * @return array Block metadata.
861 */
862 protected static function get_blocks_metadata() {
863 // NOTE: the compat/6.1 version of this method in Gutenberg did not have these changes.
864 $registry = WP_Block_Type_Registry::get_instance();
865 $blocks = $registry->get_all_registered();
866
867 // Is there metadata for all currently registered blocks?
868 $blocks = array_diff_key( $blocks, static::$blocks_metadata );
869 if ( empty( $blocks ) ) {
870 return static::$blocks_metadata;
871 }
872
873 foreach ( $blocks as $block_name => $block_type ) {
874 $root_selector = wp_get_block_css_selector( $block_type );
875
876 static::$blocks_metadata[ $block_name ]['selector'] = $root_selector;
877 static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector );
878
879 $elements = static::get_block_element_selectors( $root_selector );
880 if ( ! empty( $elements ) ) {
881 static::$blocks_metadata[ $block_name ]['elements'] = $elements;
882 }
883
884 // The block may or may not have a duotone selector.
885 $duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' );
886
887 // Keep backwards compatibility for support.color.__experimentalDuotone.
888 if ( null === $duotone_selector ) {
889 $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null );
890
891 if ( $duotone_support ) {
892 $root_selector = wp_get_block_css_selector( $block_type );
893 $duotone_selector = WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $duotone_support );
894 }
895 }
896
897 if ( null !== $duotone_selector ) {
898 static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
899 }
900
901 // If the block has style variations, append their selectors to the block metadata.
902 if ( ! empty( $block_type->styles ) ) {
903 $style_selectors = array();
904 foreach ( $block_type->styles as $style ) {
905 // The style variation classname is duplicated in the selector to ensure that it overrides core block styles.
906 $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
907 }
908 static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
909 }
910 }
911
912 return static::$blocks_metadata;
913 }
914
915 /**
916 * Given a tree, removes the keys that are not present in the schema.
917 *
918 * It is recursive and modifies the input in-place.
919 *
920 * @since 5.8.0
921 *
922 * @param array $tree Input to process.
923 * @param array $schema Schema to adhere to.
924 * @return array The modified $tree.
925 */
926 protected static function remove_keys_not_in_schema( $tree, $schema ) {
927 $tree = array_intersect_key( $tree, $schema );
928
929 foreach ( $schema as $key => $data ) {
930 if ( ! isset( $tree[ $key ] ) ) {
931 continue;
932 }
933
934 if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
935 $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
936
937 if ( empty( $tree[ $key ] ) ) {
938 unset( $tree[ $key ] );
939 }
940 } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
941 unset( $tree[ $key ] );
942 }
943 }
944
945 return $tree;
946 }
947
948 /**
949 * Returns the existing settings for each block.
950 *
951 * Example:
952 *
953 * {
954 * 'root': {
955 * 'color': {
956 * 'custom': true
957 * }
958 * },
959 * 'core/paragraph': {
960 * 'spacing': {
961 * 'customPadding': true
962 * }
963 * }
964 * }
965 *
966 * @since 5.8.0
967 *
968 * @return array Settings per block.
969 */
970 public function get_settings() {
971 if ( ! isset( $this->theme_json['settings'] ) ) {
972 return array();
973 } else {
974 return $this->theme_json['settings'];
975 }
976 }
977
978 /**
979 * Returns the stylesheet that results of processing
980 * the theme.json structure this object represents.
981 *
982 * @since 5.8.0
983 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
984 *
985 * @param array $types Types of styles to load. Will load all by default. It accepts:
986 * - `variables`: only the CSS Custom Properties for presets & custom ones.
987 * - `styles`: only the styles section in theme.json.
988 * - `presets`: only the classes for the presets.
989 * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
990 * @param array $options An array of options for now used for internal purposes only (may change without notice).
991 * The options currently supported are 'scope' that makes sure all style are scoped to a given selector,
992 * and root_selector which overwrites and forces a given selector to be used on the root node.
993 * @return string The resulting stylesheet.
994 */
995 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
996 if ( null === $origins ) {
997 $origins = static::VALID_ORIGINS;
998 }
999
1000 if ( is_string( $types ) ) {
1001 // Dispatch error and map old arguments to new ones.
1002 _deprecated_argument( __FUNCTION__, '5.9.0' );
1003 if ( 'block_styles' === $types ) {
1004 $types = array( 'styles', 'presets' );
1005 } elseif ( 'css_variables' === $types ) {
1006 $types = array( 'variables' );
1007 } else {
1008 $types = array( 'variables', 'styles', 'presets' );
1009 }
1010 }
1011
1012 $blocks_metadata = static::get_blocks_metadata();
1013 $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
1014 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
1015
1016 $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
1017 $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
1018
1019 if ( ! empty( $options['scope'] ) ) {
1020 foreach ( $setting_nodes as &$node ) {
1021 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1022 }
1023 foreach ( $style_nodes as &$node ) {
1024 $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1025 }
1026 }
1027
1028 if ( ! empty( $options['root_selector'] ) ) {
1029 if ( false !== $root_settings_key ) {
1030 $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
1031 }
1032 if ( false !== $root_style_key ) {
1033 $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
1034 }
1035 }
1036
1037 $stylesheet = '';
1038
1039 if ( in_array( 'variables', $types, true ) ) {
1040 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
1041 }
1042
1043 if ( in_array( 'styles', $types, true ) ) {
1044 if ( false !== $root_style_key ) {
1045 $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
1046 }
1047 $stylesheet .= $this->get_block_classes( $style_nodes );
1048 } elseif ( in_array( 'base-layout-styles', $types, true ) ) {
1049 $root_selector = static::ROOT_BLOCK_SELECTOR;
1050 $columns_selector = '.wp-block-columns';
1051 if ( ! empty( $options['scope'] ) ) {
1052 $root_selector = static::scope_selector( $options['scope'], $root_selector );
1053 $columns_selector = static::scope_selector( $options['scope'], $columns_selector );
1054 }
1055 if ( ! empty( $options['root_selector'] ) ) {
1056 $root_selector = $options['root_selector'];
1057 }
1058 // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
1059 // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
1060 $base_styles_nodes = array(
1061 array(
1062 'path' => array( 'styles' ),
1063 'selector' => $root_selector,
1064 ),
1065 array(
1066 'path' => array( 'styles', 'blocks', 'core/columns' ),
1067 'selector' => $columns_selector,
1068 'name' => 'core/columns',
1069 ),
1070 );
1071
1072 foreach ( $base_styles_nodes as $base_style_node ) {
1073 $stylesheet .= $this->get_layout_styles( $base_style_node );
1074 }
1075 }
1076
1077 if ( in_array( 'presets', $types, true ) ) {
1078 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
1079 }
1080
1081 return $stylesheet;
1082 }
1083
1084 /**
1085 * Processes the CSS, to apply nesting.
1086 *
1087 * @since 6.2.0
1088 *
1089 * @param string $css The CSS to process.
1090 * @param string $selector The selector to nest.
1091 * @return string The processed CSS.
1092 */
1093 protected function process_blocks_custom_css( $css, $selector ) {
1094 $processed_css = '';
1095
1096 // Split CSS nested rules.
1097 $parts = explode( '&', $css );
1098 foreach ( $parts as $part ) {
1099 $processed_css .= ( ! str_contains( $part, '{' ) )
1100 ? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level.
1101 : trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character.
1102 }
1103 return $processed_css;
1104 }
1105
1106 /**
1107 * Returns the global styles custom css.
1108 *
1109 * @since 6.2.0
1110 *
1111 * @return string The global styles custom CSS.
1112 */
1113 public function get_custom_css() {
1114 // Add the global styles root CSS.
1115 $stylesheet = _wp_array_get( $this->theme_json, array( 'styles', 'css' ), '' );
1116
1117 // Add the global styles block CSS.
1118 if ( isset( $this->theme_json['styles']['blocks'] ) ) {
1119 foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
1120 $custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) );
1121 if ( $custom_block_css ) {
1122 $selector = static::$blocks_metadata[ $name ]['selector'];
1123 $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
1124 }
1125 }
1126 }
1127
1128 return $stylesheet;
1129 }
1130
1131 /**
1132 * Returns the page templates of the active theme.
1133 *
1134 * @since 5.9.0
1135 *
1136 * @return array
1137 */
1138 public function get_custom_templates() {
1139 $custom_templates = array();
1140 if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
1141 return $custom_templates;
1142 }
1143
1144 foreach ( $this->theme_json['customTemplates'] as $item ) {
1145 if ( isset( $item['name'] ) ) {
1146 $custom_templates[ $item['name'] ] = array(
1147 'title' => isset( $item['title'] ) ? $item['title'] : '',
1148 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
1149 );
1150 }
1151 }
1152 return $custom_templates;
1153 }
1154
1155 /**
1156 * Returns the template part data of active theme.
1157 *
1158 * @since 5.9.0
1159 *
1160 * @return array
1161 */
1162 public function get_template_parts() {
1163 $template_parts = array();
1164 if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
1165 return $template_parts;
1166 }
1167
1168 foreach ( $this->theme_json['templateParts'] as $item ) {
1169 if ( isset( $item['name'] ) ) {
1170 $template_parts[ $item['name'] ] = array(
1171 'title' => isset( $item['title'] ) ? $item['title'] : '',
1172 'area' => isset( $item['area'] ) ? $item['area'] : '',
1173 );
1174 }
1175 }
1176 return $template_parts;
1177 }
1178
1179 /**
1180 * Converts each style section into a list of rulesets
1181 * containing the block styles to be appended to the stylesheet.
1182 *
1183 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
1184 *
1185 * For each section this creates a new ruleset such as:
1186 *
1187 * block-selector {
1188 * style-property-one: value;
1189 * }
1190 *
1191 * @since 5.8.0 As `get_block_styles()`.
1192 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
1193 * and no longer returns preset classes.
1194 * Removed the `$setting_nodes` parameter.
1195 * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`.
1196 *
1197 * @param array $style_nodes Nodes with styles.
1198 * @return string The new stylesheet.
1199 */
1200 protected function get_block_classes( $style_nodes ) {
1201 $block_rules = '';
1202
1203 foreach ( $style_nodes as $metadata ) {
1204 if ( null === $metadata['selector'] ) {
1205 continue;
1206 }
1207 $block_rules .= static::get_styles_for_block( $metadata );
1208 }
1209
1210 return $block_rules;
1211 }
1212
1213 /**
1214 * Gets the CSS layout rules for a particular block from theme.json layout definitions.
1215 *
1216 * @since 6.1.0
1217 *
1218 * @param array $block_metadata Metadata about the block to get styles for.
1219 * @return string Layout styles for the block.
1220 */
1221 protected function get_layout_styles( $block_metadata ) {
1222 $block_rules = '';
1223 $block_type = null;
1224
1225 // Skip outputting layout styles if explicitly disabled.
1226 if ( current_theme_supports( 'disable-layout-styles' ) ) {
1227 return $block_rules;
1228 }
1229
1230 if ( isset( $block_metadata['name'] ) ) {
1231 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
1232 if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) {
1233 return $block_rules;
1234 }
1235 }
1236
1237 $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
1238 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
1239 $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.
1240 $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
1241 $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() );
1242 $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
1243
1244 // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1245 // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1246 if ( $has_block_gap_support || $has_fallback_gap_support ) {
1247 $block_gap_value = null;
1248 // Use a fallback gap value if block gap support is not available.
1249 if ( ! $has_block_gap_support ) {
1250 $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1251 if ( ! empty( $block_type ) ) {
1252 $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null );
1253 }
1254 } else {
1255 $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1256 }
1257
1258 // Support split row / column values and concatenate to a shorthand value.
1259 if ( is_array( $block_gap_value ) ) {
1260 if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1261 $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1262 $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1263 $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
1264 } else {
1265 // Skip outputting gap value if not all sides are provided.
1266 $block_gap_value = null;
1267 }
1268 }
1269
1270 // If the block should have custom gap, add the gap styles.
1271 if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) {
1272 foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
1273 // Allow outputting fallback gap styles for flex layout type when block gap support isn't available.
1274 if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) {
1275 continue;
1276 }
1277
1278 $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) );
1279 $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() );
1280
1281 if (
1282 ! empty( $class_name ) &&
1283 ! empty( $spacing_rules )
1284 ) {
1285 foreach ( $spacing_rules as $spacing_rule ) {
1286 $declarations = array();
1287 if (
1288 isset( $spacing_rule['selector'] ) &&
1289 preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
1290 ! empty( $spacing_rule['rules'] )
1291 ) {
1292 // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1293 foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1294 $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
1295 if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1296 $declarations[] = array(
1297 'name' => $css_property,
1298 'value' => $current_css_value,
1299 );
1300 }
1301 }
1302
1303 if ( ! $has_block_gap_support ) {
1304 // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
1305 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)';
1306 $layout_selector = sprintf(
1307 $format,
1308 $selector,
1309 $class_name,
1310 $spacing_rule['selector']
1311 );
1312 } else {
1313 $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(%s .%s) %s' : '%s-%s%s';
1314 $layout_selector = sprintf(
1315 $format,
1316 $selector,
1317 $class_name,
1318 $spacing_rule['selector']
1319 );
1320 }
1321 $block_rules .= static::to_ruleset( $layout_selector, $declarations );
1322 }
1323 }
1324 }
1325 }
1326 }
1327 }
1328
1329 // Output base styles.
1330 if (
1331 static::ROOT_BLOCK_SELECTOR === $selector
1332 ) {
1333 $valid_display_modes = array( 'block', 'flex', 'grid' );
1334 foreach ( $layout_definitions as $layout_definition ) {
1335 $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) );
1336 $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() );
1337
1338 if (
1339 ! empty( $class_name ) &&
1340 is_array( $base_style_rules )
1341 ) {
1342 // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
1343 if (
1344 ! empty( $layout_definition['displayMode'] ) &&
1345 is_string( $layout_definition['displayMode'] ) &&
1346 in_array( $layout_definition['displayMode'], $valid_display_modes, true )
1347 ) {
1348 $layout_selector = sprintf(
1349 '%s .%s',
1350 $selector,
1351 $class_name
1352 );
1353 $block_rules .= static::to_ruleset(
1354 $layout_selector,
1355 array(
1356 array(
1357 'name' => 'display',
1358 'value' => $layout_definition['displayMode'],
1359 ),
1360 )
1361 );
1362 }
1363
1364 foreach ( $base_style_rules as $base_style_rule ) {
1365 $declarations = array();
1366
1367 if (
1368 isset( $base_style_rule['selector'] ) &&
1369 preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
1370 ! empty( $base_style_rule['rules'] )
1371 ) {
1372 foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
1373 if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
1374 $declarations[] = array(
1375 'name' => $css_property,
1376 'value' => $css_value,
1377 );
1378 }
1379 }
1380
1381 $layout_selector = sprintf(
1382 '%s .%s%s',
1383 $selector,
1384 $class_name,
1385 $base_style_rule['selector']
1386 );
1387 $block_rules .= static::to_ruleset( $layout_selector, $declarations );
1388 }
1389 }
1390 }
1391 }
1392 }
1393 return $block_rules;
1394 }
1395
1396 /**
1397 * Creates new rulesets as classes for each preset value such as:
1398 *
1399 * .has-value-color {
1400 * color: value;
1401 * }
1402 *
1403 * .has-value-background-color {
1404 * background-color: value;
1405 * }
1406 *
1407 * .has-value-font-size {
1408 * font-size: value;
1409 * }
1410 *
1411 * .has-value-gradient-background {
1412 * background: value;
1413 * }
1414 *
1415 * p.has-value-gradient-background {
1416 * background: value;
1417 * }
1418 *
1419 * @since 5.9.0
1420 *
1421 * @param array $setting_nodes Nodes with settings.
1422 * @param array $origins List of origins to process presets from.
1423 * @return string The new stylesheet.
1424 */
1425 protected function get_preset_classes( $setting_nodes, $origins ) {
1426 $preset_rules = '';
1427
1428 foreach ( $setting_nodes as $metadata ) {
1429 if ( null === $metadata['selector'] ) {
1430 continue;
1431 }
1432
1433 $selector = $metadata['selector'];
1434 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1435 $preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
1436 }
1437
1438 return $preset_rules;
1439 }
1440
1441 /**
1442 * Converts each styles section into a list of rulesets
1443 * to be appended to the stylesheet.
1444 * These rulesets contain all the css variables (custom variables and preset variables).
1445 *
1446 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
1447 *
1448 * For each section this creates a new ruleset such as:
1449 *
1450 * block-selector {
1451 * --wp--preset--category--slug: value;
1452 * --wp--custom--variable: value;
1453 * }
1454 *
1455 * @since 5.8.0
1456 * @since 5.9.0 Added the `$origins` parameter.
1457 *
1458 * @param array $nodes Nodes with settings.
1459 * @param array $origins List of origins to process.
1460 * @return string The new stylesheet.
1461 */
1462 protected function get_css_variables( $nodes, $origins ) {
1463 $stylesheet = '';
1464 foreach ( $nodes as $metadata ) {
1465 if ( null === $metadata['selector'] ) {
1466 continue;
1467 }
1468
1469 $selector = $metadata['selector'];
1470
1471 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1472 $declarations = static::compute_preset_vars( $node, $origins );
1473 $theme_vars_declarations = static::compute_theme_vars( $node );
1474 foreach ( $theme_vars_declarations as $theme_vars_declaration ) {
1475 $declarations[] = $theme_vars_declaration;
1476 }
1477
1478 $stylesheet .= static::to_ruleset( $selector, $declarations );
1479 }
1480
1481 return $stylesheet;
1482 }
1483
1484 /**
1485 * Given a selector and a declaration list,
1486 * creates the corresponding ruleset.
1487 *
1488 * @since 5.8.0
1489 *
1490 * @param string $selector CSS selector.
1491 * @param array $declarations List of declarations.
1492 * @return string The resulting CSS ruleset.
1493 */
1494 protected static function to_ruleset( $selector, $declarations ) {
1495 if ( empty( $declarations ) ) {
1496 return '';
1497 }
1498
1499 $declaration_block = array_reduce(
1500 $declarations,
1501 static function ( $carry, $element ) {
1502 return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
1503 ''
1504 );
1505
1506 return $selector . '{' . $declaration_block . '}';
1507 }
1508
1509 /**
1510 * Given a settings array, returns the generated rulesets
1511 * for the preset classes.
1512 *
1513 * @since 5.8.0
1514 * @since 5.9.0 Added the `$origins` parameter.
1515 *
1516 * @param array $settings Settings to process.
1517 * @param string $selector Selector wrapping the classes.
1518 * @param array $origins List of origins to process.
1519 * @return string The result of processing the presets.
1520 */
1521 protected static function compute_preset_classes( $settings, $selector, $origins ) {
1522 if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
1523 // Classes at the global level do not need any CSS prefixed,
1524 // and we don't want to increase its specificity.
1525 $selector = '';
1526 }
1527
1528 $stylesheet = '';
1529 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1530 $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
1531 foreach ( $preset_metadata['classes'] as $class => $property ) {
1532 foreach ( $slugs as $slug ) {
1533 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
1534 $class_name = static::replace_slug_in_string( $class, $slug );
1535 $stylesheet .= static::to_ruleset(
1536 static::append_to_selector( $selector, $class_name ),
1537 array(
1538 array(
1539 'name' => $property,
1540 'value' => 'var(' . $css_var . ') !important',
1541 ),
1542 )
1543 );
1544 }
1545 }
1546 }
1547
1548 return $stylesheet;
1549 }
1550
1551 /**
1552 * Function that scopes a selector with another one. This works a bit like
1553 * SCSS nesting except the `&` operator isn't supported.
1554 *
1555 * <code>
1556 * $scope = '.a, .b .c';
1557 * $selector = '> .x, .y';
1558 * $merged = scope_selector( $scope, $selector );
1559 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
1560 * </code>
1561 *
1562 * @since 5.9.0
1563 *
1564 * @param string $scope Selector to scope to.
1565 * @param string $selector Original selector.
1566 * @return string Scoped selector.
1567 */
1568 public static function scope_selector( $scope, $selector ) {
1569 $scopes = explode( ',', $scope );
1570 $selectors = explode( ',', $selector );
1571
1572 $selectors_scoped = array();
1573 foreach ( $scopes as $outer ) {
1574 foreach ( $selectors as $inner ) {
1575 $outer = trim( $outer );
1576 $inner = trim( $inner );
1577 if ( ! empty( $outer ) && ! empty( $inner ) ) {
1578 $selectors_scoped[] = $outer . ' ' . $inner;
1579 } elseif ( empty( $outer ) ) {
1580 $selectors_scoped[] = $inner;
1581 } elseif ( empty( $inner ) ) {
1582 $selectors_scoped[] = $outer;
1583 }
1584 }
1585 }
1586
1587 $result = implode( ', ', $selectors_scoped );
1588 return $result;
1589 }
1590
1591 /**
1592 * Gets preset values keyed by slugs based on settings and metadata.
1593 *
1594 * <code>
1595 * $settings = array(
1596 * 'typography' => array(
1597 * 'fontFamilies' => array(
1598 * array(
1599 * 'slug' => 'sansSerif',
1600 * 'fontFamily' => '"Helvetica Neue", sans-serif',
1601 * ),
1602 * array(
1603 * 'slug' => 'serif',
1604 * 'colors' => 'Georgia, serif',
1605 * )
1606 * ),
1607 * ),
1608 * );
1609 * $meta = array(
1610 * 'path' => array( 'typography', 'fontFamilies' ),
1611 * 'value_key' => 'fontFamily',
1612 * );
1613 * $values_by_slug = get_settings_values_by_slug();
1614 * // $values_by_slug === array(
1615 * // 'sans-serif' => '"Helvetica Neue", sans-serif',
1616 * // 'serif' => 'Georgia, serif',
1617 * // );
1618 * </code>
1619 *
1620 * @since 5.9.0
1621 *
1622 * @param array $settings Settings to process.
1623 * @param array $preset_metadata One of the PRESETS_METADATA values.
1624 * @param array $origins List of origins to process.
1625 * @return array Array of presets where each key is a slug and each value is the preset value.
1626 */
1627 protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
1628 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
1629
1630 $result = array();
1631 foreach ( $origins as $origin ) {
1632 if ( ! isset( $preset_per_origin[ $origin ] ) ) {
1633 continue;
1634 }
1635 foreach ( $preset_per_origin[ $origin ] as $preset ) {
1636 $slug = _wp_to_kebab_case( $preset['slug'] );
1637
1638 $value = '';
1639 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
1640 $value_key = $preset_metadata['value_key'];
1641 $value = $preset[ $value_key ];
1642 } elseif (
1643 isset( $preset_metadata['value_func'] ) &&
1644 is_callable( $preset_metadata['value_func'] )
1645 ) {
1646 $value_func = $preset_metadata['value_func'];
1647 $value = call_user_func( $value_func, $preset );
1648 } else {
1649 // If we don't have a value, then don't add it to the result.
1650 continue;
1651 }
1652
1653 $result[ $slug ] = $value;
1654 }
1655 }
1656 return $result;
1657 }
1658
1659 /**
1660 * Similar to get_settings_values_by_slug, but doesn't compute the value.
1661 *
1662 * @since 5.9.0
1663 *
1664 * @param array $settings Settings to process.
1665 * @param array $preset_metadata One of the PRESETS_METADATA values.
1666 * @param array $origins List of origins to process.
1667 * @return array Array of presets where the key and value are both the slug.
1668 */
1669 protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
1670 if ( null === $origins ) {
1671 $origins = static::VALID_ORIGINS;
1672 }
1673
1674 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
1675
1676 $result = array();
1677 foreach ( $origins as $origin ) {
1678 if ( ! isset( $preset_per_origin[ $origin ] ) ) {
1679 continue;
1680 }
1681 foreach ( $preset_per_origin[ $origin ] as $preset ) {
1682 $slug = _wp_to_kebab_case( $preset['slug'] );
1683
1684 // Use the array as a set so we don't get duplicates.
1685 $result[ $slug ] = $slug;
1686 }
1687 }
1688 return $result;
1689 }
1690
1691 /**
1692 * Transforms a slug into a CSS Custom Property.
1693 *
1694 * @since 5.9.0
1695 *
1696 * @param string $input String to replace.
1697 * @param string $slug The slug value to use to generate the custom property.
1698 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
1699 */
1700 protected static function replace_slug_in_string( $input, $slug ) {
1701 return strtr( $input, array( '$slug' => $slug ) );
1702 }
1703
1704 /**
1705 * Given the block settings, extracts the CSS Custom Properties
1706 * for the presets and adds them to the $declarations array
1707 * following the format:
1708 *
1709 * ```php
1710 * array(
1711 * 'name' => 'property_name',
1712 * 'value' => 'property_value,
1713 * )
1714 * ```
1715 *
1716 * @since 5.8.0
1717 * @since 5.9.0 Added the `$origins` parameter.
1718 *
1719 * @param array $settings Settings to process.
1720 * @param array $origins List of origins to process.
1721 * @return array The modified $declarations.
1722 */
1723 protected static function compute_preset_vars( $settings, $origins ) {
1724 $declarations = array();
1725 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1726 $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
1727 foreach ( $values_by_slug as $slug => $value ) {
1728 $declarations[] = array(
1729 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
1730 'value' => $value,
1731 );
1732 }
1733 }
1734
1735 return $declarations;
1736 }
1737
1738 /**
1739 * Given an array of settings, extracts the CSS Custom Properties
1740 * for the custom values and adds them to the $declarations
1741 * array following the format:
1742 *
1743 * ```php
1744 * array(
1745 * 'name' => 'property_name',
1746 * 'value' => 'property_value,
1747 * )
1748 * ```
1749 *
1750 * @since 5.8.0
1751 *
1752 * @param array $settings Settings to process.
1753 * @return array The modified $declarations.
1754 */
1755 protected static function compute_theme_vars( $settings ) {
1756 $declarations = array();
1757 $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
1758 $css_vars = static::flatten_tree( $custom_values );
1759 foreach ( $css_vars as $key => $value ) {
1760 $declarations[] = array(
1761 'name' => '--wp--custom--' . $key,
1762 'value' => $value,
1763 );
1764 }
1765
1766 return $declarations;
1767 }
1768
1769 /**
1770 * Given a tree, it creates a flattened one
1771 * by merging the keys and binding the leaf values
1772 * to the new keys.
1773 *
1774 * It also transforms camelCase names into kebab-case
1775 * and substitutes '/' by '-'.
1776 *
1777 * This is thought to be useful to generate
1778 * CSS Custom Properties from a tree,
1779 * although there's nothing in the implementation
1780 * of this function that requires that format.
1781 *
1782 * For example, assuming the given prefix is '--wp'
1783 * and the token is '--', for this input tree:
1784 *
1785 * {
1786 * 'some/property': 'value',
1787 * 'nestedProperty': {
1788 * 'sub-property': 'value'
1789 * }
1790 * }
1791 *
1792 * it'll return this output:
1793 *
1794 * {
1795 * '--wp--some-property': 'value',
1796 * '--wp--nested-property--sub-property': 'value'
1797 * }
1798 *
1799 * @since 5.8.0
1800 *
1801 * @param array $tree Input tree to process.
1802 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
1803 * @param string $token Optional. Token to use between levels. Default '--'.
1804 * @return array The flattened tree.
1805 */
1806 protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
1807 $result = array();
1808 foreach ( $tree as $property => $value ) {
1809 $new_key = $prefix . str_replace(
1810 '/',
1811 '-',
1812 strtolower( _wp_to_kebab_case( $property ) )
1813 );
1814
1815 if ( is_array( $value ) ) {
1816 $new_prefix = $new_key . $token;
1817 $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
1818 foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
1819 $result[ $subtree_key ] = $subtree_value;
1820 }
1821 } else {
1822 $result[ $new_key ] = $value;
1823 }
1824 }
1825 return $result;
1826 }
1827
1828 /**
1829 * Given a styles array, it extracts the style properties
1830 * and adds them to the $declarations array following the format:
1831 *
1832 * ```php
1833 * array(
1834 * 'name' => 'property_name',
1835 * 'value' => 'property_value,
1836 * )
1837 * ```
1838 *
1839 * @since 5.8.0
1840 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
1841 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
1842 *
1843 * @param array $styles Styles to process.
1844 * @param array $settings Theme settings.
1845 * @param array $properties Properties metadata.
1846 * @param array $theme_json Theme JSON array.
1847 * @param string $selector The style block selector.
1848 * @param boolean $use_root_padding Whether to add custom properties at root level.
1849 * @return array Returns the modified $declarations.
1850 */
1851 protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
1852 if ( null === $properties ) {
1853 $properties = static::PROPERTIES_METADATA;
1854 }
1855
1856 $declarations = array();
1857 if ( empty( $styles ) ) {
1858 return $declarations;
1859 }
1860
1861 $root_variable_duplicates = array();
1862
1863 foreach ( $properties as $css_property => $value_path ) {
1864 $value = static::get_property_value( $styles, $value_path, $theme_json );
1865
1866 if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
1867 continue;
1868 }
1869 // Root-level padding styles don't currently support strings with CSS shorthand values.
1870 // This may change: https://github.com/WordPress/gutenberg/issues/40132.
1871 if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
1872 continue;
1873 }
1874
1875 if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
1876 $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
1877 }
1878
1879 // Look up protected properties, keyed by value path.
1880 // Skip protected properties that are explicitly set to `null`.
1881 if ( is_array( $value_path ) ) {
1882 $path_string = implode( '.', $value_path );
1883 if (
1884 // TODO: Replace array_key_exists() with isset() check once WordPress drops
1885 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
1886 array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
1887 _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
1888 ) {
1889 continue;
1890 }
1891 }
1892
1893 // Skip if empty and not "0" or value represents array of longhand values.
1894 $has_missing_value = empty( $value ) && ! is_numeric( $value );
1895 if ( $has_missing_value || is_array( $value ) ) {
1896 continue;
1897 }
1898
1899 // Calculates fluid typography rules where available.
1900 if ( 'font-size' === $css_property ) {
1901 /*
1902 * wp_get_typography_font_size_value() will check
1903 * if fluid typography has been activated and also
1904 * whether the incoming value can be converted to a fluid value.
1905 * Values that already have a clamp() function will not pass the test,
1906 * and therefore the original $value will be returned.
1907 */
1908 $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) );
1909 }
1910
1911 $declarations[] = array(
1912 'name' => $css_property,
1913 'value' => $value,
1914 );
1915 }
1916
1917 // If a variable value is added to the root, the corresponding property should be removed.
1918 foreach ( $root_variable_duplicates as $duplicate ) {
1919 $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
1920 if ( is_numeric( $discard ) ) {
1921 array_splice( $declarations, $discard, 1 );
1922 }
1923 }
1924
1925 return $declarations;
1926 }
1927
1928 /**
1929 * Returns the style property for the given path.
1930 *
1931 * It also converts CSS Custom Property stored as
1932 * "var:preset|color|secondary" to the form
1933 * "--wp--preset--color--secondary".
1934 *
1935 * It also converts references to a path to the value
1936 * stored at that location, e.g.
1937 * { "ref": "style.color.background" } => "#fff".
1938 *
1939 * @since 5.8.0
1940 * @since 5.9.0 Added support for values of array type, which are returned as is.
1941 * @since 6.1.0 Added the `$theme_json` parameter.
1942 *
1943 * @param array $styles Styles subtree.
1944 * @param array $path Which property to process.
1945 * @param array $theme_json Theme JSON array.
1946 * @return string|array Style property value.
1947 */
1948 protected static function get_property_value( $styles, $path, $theme_json = null ) {
1949 $value = _wp_array_get( $styles, $path, '' );
1950
1951 // Gutenberg didn't have this check.
1952 if ( '' === $value || null === $value ) {
1953 // No need to process the value further.
1954 return '';
1955 }
1956
1957 /*
1958 * This converts references to a path to the value at that path
1959 * where the values is an array with a "ref" key, pointing to a path.
1960 * For example: { "ref": "style.color.background" } => "#fff".
1961 */
1962 if ( is_array( $value ) && isset( $value['ref'] ) ) {
1963 $value_path = explode( '.', $value['ref'] );
1964 $ref_value = _wp_array_get( $theme_json, $value_path );
1965 // Only use the ref value if we find anything.
1966 if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
1967 $value = $ref_value;
1968 }
1969
1970 if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
1971 $path_string = json_encode( $path );
1972 $ref_value_string = json_encode( $ref_value );
1973 _doing_it_wrong(
1974 'get_property_value',
1975 sprintf(
1976 /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
1977 __( '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' ),
1978 'theme.json',
1979 $ref_value_string,
1980 $path_string,
1981 $ref_value['ref']
1982 ),
1983 '6.1.0'
1984 );
1985 }
1986 }
1987
1988 if ( is_array( $value ) ) {
1989 return $value;
1990 }
1991
1992 return $value;
1993 }
1994
1995 /**
1996 * Builds metadata for the setting nodes, which returns in the form of:
1997 *
1998 * [
1999 * [
2000 * 'path' => ['path', 'to', 'some', 'node' ],
2001 * 'selector' => 'CSS selector for some node'
2002 * ],
2003 * [
2004 * 'path' => [ 'path', 'to', 'other', 'node' ],
2005 * 'selector' => 'CSS selector for other node'
2006 * ],
2007 * ]
2008 *
2009 * @since 5.8.0
2010 *
2011 * @param array $theme_json The tree to extract setting nodes from.
2012 * @param array $selectors List of selectors per block.
2013 * @return array An array of setting nodes metadata.
2014 */
2015 protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
2016 $nodes = array();
2017 if ( ! isset( $theme_json['settings'] ) ) {
2018 return $nodes;
2019 }
2020
2021 // Top-level.
2022 $nodes[] = array(
2023 'path' => array( 'settings' ),
2024 'selector' => static::ROOT_BLOCK_SELECTOR,
2025 );
2026
2027 // Calculate paths for blocks.
2028 if ( ! isset( $theme_json['settings']['blocks'] ) ) {
2029 return $nodes;
2030 }
2031
2032 foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
2033 $selector = null;
2034 if ( isset( $selectors[ $name ]['selector'] ) ) {
2035 $selector = $selectors[ $name ]['selector'];
2036 }
2037
2038 $nodes[] = array(
2039 'path' => array( 'settings', 'blocks', $name ),
2040 'selector' => $selector,
2041 );
2042 }
2043
2044 return $nodes;
2045 }
2046
2047 /**
2048 * Builds metadata for the style nodes, which returns in the form of:
2049 *
2050 * [
2051 * [
2052 * 'path' => [ 'path', 'to', 'some', 'node' ],
2053 * 'selector' => 'CSS selector for some node',
2054 * 'duotone' => 'CSS selector for duotone for some node'
2055 * ],
2056 * [
2057 * 'path' => ['path', 'to', 'other', 'node' ],
2058 * 'selector' => 'CSS selector for other node',
2059 * 'duotone' => null
2060 * ],
2061 * ]
2062 *
2063 * @since 5.8.0
2064 *
2065 * @param array $theme_json The tree to extract style nodes from.
2066 * @param array $selectors List of selectors per block.
2067 * @return array An array of style nodes metadata.
2068 */
2069 protected static function get_style_nodes( $theme_json, $selectors = array() ) {
2070 $nodes = array();
2071 if ( ! isset( $theme_json['styles'] ) ) {
2072 return $nodes;
2073 }
2074
2075 // Top-level.
2076 $nodes[] = array(
2077 'path' => array( 'styles' ),
2078 'selector' => static::ROOT_BLOCK_SELECTOR,
2079 );
2080
2081 if ( isset( $theme_json['styles']['elements'] ) ) {
2082 foreach ( self::ELEMENTS as $element => $selector ) {
2083 if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) {
2084 continue;
2085 }
2086
2087 // Handle element defaults.
2088 $nodes[] = array(
2089 'path' => array( 'styles', 'elements', $element ),
2090 'selector' => static::ELEMENTS[ $element ],
2091 );
2092
2093 // Handle any pseudo selectors for the element.
2094 // TODO: Replace array_key_exists() with isset() check once WordPress drops
2095 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2096 if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2097 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
2098
2099 if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
2100 $nodes[] = array(
2101 'path' => array( 'styles', 'elements', $element ),
2102 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
2103 );
2104 }
2105 }
2106 }
2107 }
2108 }
2109
2110 // Blocks.
2111 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
2112 return $nodes;
2113 }
2114
2115 $block_nodes = static::get_block_nodes( $theme_json, $selectors );
2116 foreach ( $block_nodes as $block_node ) {
2117 $nodes[] = $block_node;
2118 }
2119
2120 /**
2121 * Filters the list of style nodes with metadata.
2122 *
2123 * This allows for things like loading block CSS independently.
2124 *
2125 * @since 6.1.0
2126 *
2127 * @param array $nodes Style nodes with metadata.
2128 */
2129 return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
2130 }
2131
2132 /**
2133 * A public helper to get the block nodes from a theme.json file.
2134 *
2135 * @since 6.1.0
2136 *
2137 * @return array The block nodes in theme.json.
2138 */
2139 public function get_styles_block_nodes() {
2140 return static::get_block_nodes( $this->theme_json );
2141 }
2142
2143 /**
2144 * Returns a filtered declarations array if there is a separator block with only a background
2145 * style defined in theme.json by adding a color attribute to reflect the changes in the front.
2146 *
2147 * @since 6.1.1
2148 *
2149 * @param array $declarations List of declarations.
2150 * @return array $declarations List of declarations filtered.
2151 */
2152 private static function update_separator_declarations( $declarations ) {
2153 // Gutenberg and core implementation differed.
2154 // https://github.com/WordPress/gutenberg/pull/44943.
2155 $background_color = '';
2156 $border_color_matches = false;
2157 $text_color_matches = false;
2158
2159 foreach ( $declarations as $declaration ) {
2160 if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
2161 $background_color = $declaration['value'];
2162 } elseif ( 'border-color' === $declaration['name'] ) {
2163 $border_color_matches = true;
2164 } elseif ( 'color' === $declaration['name'] ) {
2165 $text_color_matches = true;
2166 }
2167
2168 if ( $background_color && $border_color_matches && $text_color_matches ) {
2169 break;
2170 }
2171 }
2172
2173 if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
2174 $declarations[] = array(
2175 'name' => 'color',
2176 'value' => $background_color,
2177 );
2178 }
2179
2180 return $declarations;
2181 }
2182
2183 /**
2184 * An internal method to get the block nodes from a theme.json file.
2185 *
2186 * @since 6.1.0
2187 *
2188 * @param array $theme_json The theme.json converted to an array.
2189 * @param array $selectors Optional list of selectors per block.
2190 * @return array The block nodes in theme.json.
2191 */
2192 private static function get_block_nodes( $theme_json, $selectors = array() ) {
2193 $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
2194 $nodes = array();
2195 if ( ! isset( $theme_json['styles'] ) ) {
2196 return $nodes;
2197 }
2198
2199 // Blocks.
2200 if ( ! isset( $theme_json['styles']['blocks'] ) ) {
2201 return $nodes;
2202 }
2203
2204 foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
2205 $selector = null;
2206 if ( isset( $selectors[ $name ]['selector'] ) ) {
2207 $selector = $selectors[ $name ]['selector'];
2208 }
2209
2210 $duotone_selector = null;
2211 if ( isset( $selectors[ $name ]['duotone'] ) ) {
2212 $duotone_selector = $selectors[ $name ]['duotone'];
2213 }
2214
2215 $feature_selectors = null;
2216 if ( isset( $selectors[ $name ]['selectors'] ) ) {
2217 $feature_selectors = $selectors[ $name ]['selectors'];
2218 }
2219
2220 $variation_selectors = array();
2221 if ( isset( $node['variations'] ) ) {
2222 foreach ( $node['variations'] as $variation => $node ) {
2223 $variation_selectors[] = array(
2224 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
2225 'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
2226 );
2227 }
2228 }
2229
2230 $nodes[] = array(
2231 'name' => $name,
2232 'path' => array( 'styles', 'blocks', $name ),
2233 'selector' => $selector,
2234 'selectors' => $feature_selectors,
2235 'duotone' => $duotone_selector,
2236 'variations' => $variation_selectors,
2237 );
2238
2239 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
2240 foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
2241 $nodes[] = array(
2242 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
2243 'selector' => $selectors[ $name ]['elements'][ $element ],
2244 );
2245
2246 // Handle any pseudo selectors for the element.
2247 // TODO: Replace array_key_exists() with isset() check once WordPress drops
2248 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2249 if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2250 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
2251 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
2252 $nodes[] = array(
2253 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
2254 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
2255 );
2256 }
2257 }
2258 }
2259 }
2260 }
2261 }
2262
2263 return $nodes;
2264 }
2265
2266 /**
2267 * Gets the CSS rules for a particular block from theme.json.
2268 *
2269 * @since 6.1.0
2270 *
2271 * @param array $block_metadata Metadata about the block to get styles for.
2272 *
2273 * @return string Styles for the block.
2274 */
2275 public function get_styles_for_block( $block_metadata ) {
2276 $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
2277 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
2278 $selector = $block_metadata['selector'];
2279 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
2280
2281 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
2282
2283 // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
2284 $style_variation_declarations = array();
2285 if ( ! empty( $block_metadata['variations'] ) ) {
2286 foreach ( $block_metadata['variations'] as $style_variation ) {
2287 $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
2288 $clean_style_variation_selector = trim( $style_variation['selector'] );
2289
2290 // Generate any feature/subfeature style declarations for the current style variation.
2291 $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
2292
2293 // Combine selectors with style variation's selector and add to overall style variation declarations.
2294 foreach ( $variation_declarations as $current_selector => $new_declarations ) {
2295 // If current selector includes block classname, remove it but leave the whitespace in.
2296 $shortened_selector = str_replace( $block_metadata['selector'] . ' ', ' ', $current_selector );
2297
2298 // Prepend the variation selector to the current selector.
2299 $split_selectors = explode( ',', $shortened_selector );
2300 $updated_selectors = array_map(
2301 static function( $split_selector ) use ( $clean_style_variation_selector ) {
2302 return $clean_style_variation_selector . $split_selector;
2303 },
2304 $split_selectors
2305 );
2306 $combined_selectors = implode( ',', $updated_selectors );
2307
2308 // Add the new declarations to the overall results under the modified selector.
2309 $style_variation_declarations[ $combined_selectors ] = $new_declarations;
2310 }
2311
2312 // Compute declarations for remaining styles not covered by feature level selectors.
2313 $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
2314 }
2315 }
2316
2317 /*
2318 * Get a reference to element name from path.
2319 * $block_metadata['path'] = array( 'styles','elements','link' );
2320 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
2321 * Skip non-element paths like just ['styles'].
2322 */
2323 $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
2324
2325 $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
2326
2327 $element_pseudo_allowed = array();
2328
2329 // TODO: Replace array_key_exists() with isset() check once WordPress drops
2330 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2331 if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2332 $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
2333 }
2334
2335 /*
2336 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
2337 * This also resets the array keys.
2338 */
2339 $pseudo_matches = array_values(
2340 array_filter(
2341 $element_pseudo_allowed,
2342 function( $pseudo_selector ) use ( $selector ) {
2343 return str_contains( $selector, $pseudo_selector );
2344 }
2345 )
2346 );
2347
2348 $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
2349
2350 /*
2351 * If the current selector is a pseudo selector that's defined in the allow list for the current
2352 * element then compute the style properties for it.
2353 * Otherwise just compute the styles for the default selector as normal.
2354 */
2355 if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
2356 // TODO: Replace array_key_exists() with isset() check once WordPress drops
2357 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2358 array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS )
2359 && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
2360 ) {
2361 $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
2362 } else {
2363 $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
2364 }
2365
2366 $block_rules = '';
2367
2368 /*
2369 * 1. Separate the declarations that use the general selector
2370 * from the ones using the duotone selector.
2371 */
2372 $declarations_duotone = array();
2373 foreach ( $declarations as $index => $declaration ) {
2374 if ( 'filter' === $declaration['name'] ) {
2375 /*
2376 * 'unset' filters happen when a filter is unset
2377 * in the site-editor UI. Because the 'unset' value
2378 * in the user origin overrides the value in the
2379 * theme origin, we can skip rendering anything
2380 * here as no filter needs to be applied anymore.
2381 * So only add declarations to with values other
2382 * than 'unset'.
2383 */
2384 if ( 'unset' !== $declaration['value'] ) {
2385 $declarations_duotone[] = $declaration;
2386 }
2387 unset( $declarations[ $index ] );
2388 }
2389 }
2390
2391 // Update declarations if there are separators with only background color defined.
2392 if ( '.wp-block-separator' === $selector ) {
2393 $declarations = static::update_separator_declarations( $declarations );
2394 }
2395
2396 // 2. Generate and append the rules that use the general selector.
2397 $block_rules .= static::to_ruleset( $selector, $declarations );
2398
2399 // 3. Generate and append the rules that use the duotone selector.
2400 if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
2401 $block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
2402 }
2403
2404 // 4. Generate Layout block gap styles.
2405 if (
2406 static::ROOT_BLOCK_SELECTOR !== $selector &&
2407 ! empty( $block_metadata['name'] )
2408 ) {
2409 $block_rules .= $this->get_layout_styles( $block_metadata );
2410 }
2411
2412 // 5. Generate and append the feature level rulesets.
2413 foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
2414 $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations );
2415 }
2416
2417 // 6. Generate and append the style variation rulesets.
2418 foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
2419 $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations );
2420 }
2421
2422 return $block_rules;
2423 }
2424
2425 /**
2426 * Outputs the CSS for layout rules on the root.
2427 *
2428 * @since 6.1.0
2429 *
2430 * @param string $selector The root node selector.
2431 * @param array $block_metadata The metadata for the root block.
2432 * @return string The additional root rules CSS.
2433 */
2434 public function get_root_layout_rules( $selector, $block_metadata ) {
2435 $css = '';
2436 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
2437 $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
2438
2439 /*
2440 * Reset default browser margin on the root body element.
2441 * This is set on the root selector **before** generating the ruleset
2442 * from the `theme.json`. This is to ensure that if the `theme.json` declares
2443 * `margin` in its `spacing` declaration for the `body` element then these
2444 * user-generated values take precedence in the CSS cascade.
2445 * @link https://github.com/WordPress/gutenberg/issues/36147.
2446 */
2447 $css .= 'body { margin: 0;';
2448
2449 /*
2450 * If there are content and wide widths in theme.json, output them
2451 * as custom properties on the body element so all blocks can use them.
2452 */
2453 if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
2454 $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
2455 $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
2456 $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
2457 $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
2458 $css .= '--wp--style--global--content-size: ' . $content_size . ';';
2459 $css .= '--wp--style--global--wide-size: ' . $wide_size . ';';
2460 }
2461
2462 $css .= '}';
2463
2464 if ( $use_root_padding ) {
2465 // Top and bottom padding are applied to the outer block container.
2466 $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
2467 // Right and left padding are applied to the first container with `.has-global-padding` class.
2468 $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
2469 // Nested containers with `.has-global-padding` class do not get padding.
2470 $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }';
2471 // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
2472 $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); }';
2473 // The above rule is negated for alignfull children of nested containers.
2474 $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }';
2475 // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks.
2476 $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
2477 // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks.
2478 $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }';
2479 }
2480
2481 $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
2482 $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
2483 $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
2484
2485 $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' );
2486 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
2487 if ( $has_block_gap_support ) {
2488 $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
2489 $css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
2490 $css .= ':where(.wp-site-blocks) > :first-child:first-child { margin-block-start: 0; }';
2491 $css .= ':where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }';
2492
2493 // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
2494 $css .= "$selector { --wp--style--block-gap: $block_gap_value; }";
2495 }
2496 $css .= $this->get_layout_styles( $block_metadata );
2497
2498 return $css;
2499 }
2500
2501 /**
2502 * For metadata values that can either be booleans or paths to booleans, gets the value.
2503 *
2504 * ```php
2505 * $data = array(
2506 * 'color' => array(
2507 * 'defaultPalette' => true
2508 * )
2509 * );
2510 *
2511 * static::get_metadata_boolean( $data, false );
2512 * // => false
2513 *
2514 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
2515 * // => true
2516 * ```
2517 *
2518 * @since 6.0.0
2519 *
2520 * @param array $data The data to inspect.
2521 * @param bool|array $path Boolean or path to a boolean.
2522 * @param bool $default_value Default value if the referenced path is missing.
2523 * Default false.
2524 * @return bool Value of boolean metadata.
2525 */
2526 protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
2527 if ( is_bool( $path ) ) {
2528 return $path;
2529 }
2530
2531 if ( is_array( $path ) ) {
2532 $value = _wp_array_get( $data, $path );
2533 if ( null !== $value ) {
2534 return $value;
2535 }
2536 }
2537
2538 return $default_value;
2539 }
2540
2541 /**
2542 * Merges new incoming data.
2543 *
2544 * @since 5.8.0
2545 * @since 5.9.0 Duotone preset also has origins.
2546 *
2547 * @param WP_Theme_JSON $incoming Data to merge.
2548 */
2549 public function merge( $incoming ) {
2550 $incoming_data = $incoming->get_raw_data();
2551 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
2552
2553 /*
2554 * The array_replace_recursive algorithm merges at the leaf level,
2555 * but we don't want leaf arrays to be merged, so we overwrite it.
2556 *
2557 * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
2558 * We rather replace the existing with the incoming value, if it exists.
2559 * This is the case of spacing.units.
2560 *
2561 * For leaf values that are associative arrays it will merge them as expected.
2562 * This is also not the behavior we want for the current associative arrays (presets).
2563 * We rather replace the existing with the incoming value, if it exists.
2564 * This happens, for example, when we merge data from theme.json upon existing
2565 * theme supports or when we merge anything coming from the same source twice.
2566 * This is the case of color.palette, color.gradients, color.duotone,
2567 * typography.fontSizes, or typography.fontFamilies.
2568 *
2569 * Additionally, for some preset types, we also want to make sure the
2570 * values they introduce don't conflict with default values. We do so
2571 * by checking the incoming slugs for theme presets and compare them
2572 * with the equivalent default presets: if a slug is present as a default
2573 * we remove it from the theme presets.
2574 */
2575 $nodes = static::get_setting_nodes( $incoming_data );
2576 $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
2577 foreach ( $nodes as $node ) {
2578 // Replace the spacing.units.
2579 $path = $node['path'];
2580 $path[] = 'spacing';
2581 $path[] = 'units';
2582
2583 $content = _wp_array_get( $incoming_data, $path, null );
2584 if ( isset( $content ) ) {
2585 _wp_array_set( $this->theme_json, $path, $content );
2586 }
2587
2588 // Replace the presets.
2589 foreach ( static::PRESETS_METADATA as $preset ) {
2590 $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
2591
2592 foreach ( static::VALID_ORIGINS as $origin ) {
2593 $base_path = $node['path'];
2594 foreach ( $preset['path'] as $leaf ) {
2595 $base_path[] = $leaf;
2596 }
2597
2598 $path = $base_path;
2599 $path[] = $origin;
2600
2601 $content = _wp_array_get( $incoming_data, $path, null );
2602 if ( ! isset( $content ) ) {
2603 continue;
2604 }
2605
2606 if ( 'theme' === $origin && $preset['use_default_names'] ) {
2607 foreach ( $content as $key => $item ) {
2608 if ( ! isset( $item['name'] ) ) {
2609 $name = static::get_name_from_defaults( $item['slug'], $base_path );
2610 if ( null !== $name ) {
2611 $content[ $key ]['name'] = $name;
2612 }
2613 }
2614 }
2615 }
2616
2617 if (
2618 ( 'theme' !== $origin ) ||
2619 ( 'theme' === $origin && $override_preset )
2620 ) {
2621 _wp_array_set( $this->theme_json, $path, $content );
2622 } else {
2623 $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
2624 $slugs = array_merge_recursive( $slugs_global, $slugs_node );
2625
2626 $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
2627 $content = static::filter_slugs( $content, $slugs_for_preset );
2628 _wp_array_set( $this->theme_json, $path, $content );
2629 }
2630 }
2631 }
2632 }
2633 }
2634
2635 /**
2636 * Converts all filter (duotone) presets into SVGs.
2637 *
2638 * @since 5.9.1
2639 *
2640 * @param array $origins List of origins to process.
2641 * @return string SVG filters.
2642 */
2643 public function get_svg_filters( $origins ) {
2644 $blocks_metadata = static::get_blocks_metadata();
2645 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
2646
2647 $filters = '';
2648 foreach ( $setting_nodes as $metadata ) {
2649 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
2650 if ( empty( $node['color']['duotone'] ) ) {
2651 continue;
2652 }
2653
2654 $duotone_presets = $node['color']['duotone'];
2655
2656 foreach ( $origins as $origin ) {
2657 if ( ! isset( $duotone_presets[ $origin ] ) ) {
2658 continue;
2659 }
2660 foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
2661 $filters .= wp_get_duotone_filter_svg( $duotone_preset );
2662 }
2663 }
2664 }
2665
2666 return $filters;
2667 }
2668
2669 /**
2670 * Determines whether a presets should be overridden or not.
2671 *
2672 * @since 5.9.0
2673 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
2674 *
2675 * @param array $theme_json The theme.json like structure to inspect.
2676 * @param array $path Path to inspect.
2677 * @param bool|array $override Data to compute whether to override the preset.
2678 * @return boolean
2679 */
2680 protected static function should_override_preset( $theme_json, $path, $override ) {
2681 _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
2682
2683 if ( is_bool( $override ) ) {
2684 return $override;
2685 }
2686
2687 /*
2688 * The relationship between whether to override the defaults
2689 * and whether the defaults are enabled is inverse:
2690 *
2691 * - If defaults are enabled => theme presets should not be overridden
2692 * - If defaults are disabled => theme presets should be overridden
2693 *
2694 * For example, a theme sets defaultPalette to false,
2695 * making the default palette hidden from the user.
2696 * In that case, we want all the theme presets to be present,
2697 * so they should override the defaults.
2698 */
2699 if ( is_array( $override ) ) {
2700 $value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
2701 if ( isset( $value ) ) {
2702 return ! $value;
2703 }
2704
2705 // Search the top-level key if none was found for this node.
2706 $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
2707 if ( isset( $value ) ) {
2708 return ! $value;
2709 }
2710
2711 return true;
2712 }
2713 }
2714
2715 /**
2716 * Returns the default slugs for all the presets in an associative array
2717 * whose keys are the preset paths and the leafs is the list of slugs.
2718 *
2719 * For example:
2720 *
2721 * array(
2722 * 'color' => array(
2723 * 'palette' => array( 'slug-1', 'slug-2' ),
2724 * 'gradients' => array( 'slug-3', 'slug-4' ),
2725 * ),
2726 * )
2727 *
2728 * @since 5.9.0
2729 *
2730 * @param array $data A theme.json like structure.
2731 * @param array $node_path The path to inspect. It's 'settings' by default.
2732 * @return array
2733 */
2734 protected static function get_default_slugs( $data, $node_path ) {
2735 $slugs = array();
2736
2737 foreach ( static::PRESETS_METADATA as $metadata ) {
2738 $path = $node_path;
2739 foreach ( $metadata['path'] as $leaf ) {
2740 $path[] = $leaf;
2741 }
2742 $path[] = 'default';
2743
2744 $preset = _wp_array_get( $data, $path, null );
2745 if ( ! isset( $preset ) ) {
2746 continue;
2747 }
2748
2749 $slugs_for_preset = array();
2750 foreach ( $preset as $item ) {
2751 if ( isset( $item['slug'] ) ) {
2752 $slugs_for_preset[] = $item['slug'];
2753 }
2754 }
2755
2756 _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
2757 }
2758
2759 return $slugs;
2760 }
2761
2762 /**
2763 * Gets a `default`'s preset name by a provided slug.
2764 *
2765 * @since 5.9.0
2766 *
2767 * @param string $slug The slug we want to find a match from default presets.
2768 * @param array $base_path The path to inspect. It's 'settings' by default.
2769 * @return string|null
2770 */
2771 protected function get_name_from_defaults( $slug, $base_path ) {
2772 $path = $base_path;
2773 $path[] = 'default';
2774 $default_content = _wp_array_get( $this->theme_json, $path, null );
2775 if ( ! $default_content ) {
2776 return null;
2777 }
2778 foreach ( $default_content as $item ) {
2779 if ( $slug === $item['slug'] ) {
2780 return $item['name'];
2781 }
2782 }
2783 return null;
2784 }
2785
2786 /**
2787 * Removes the preset values whose slug is equal to any of given slugs.
2788 *
2789 * @since 5.9.0
2790 *
2791 * @param array $node The node with the presets to validate.
2792 * @param array $slugs The slugs that should not be overridden.
2793 * @return array The new node.
2794 */
2795 protected static function filter_slugs( $node, $slugs ) {
2796 if ( empty( $slugs ) ) {
2797 return $node;
2798 }
2799
2800 $new_node = array();
2801 foreach ( $node as $value ) {
2802 if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
2803 $new_node[] = $value;
2804 }
2805 }
2806
2807 return $new_node;
2808 }
2809
2810 /**
2811 * Removes insecure data from theme.json.
2812 *
2813 * @since 5.9.0
2814 *
2815 * @param array $theme_json Structure to sanitize.
2816 * @return array Sanitized structure.
2817 */
2818 public static function remove_insecure_properties( $theme_json ) {
2819 $sanitized = array();
2820
2821 $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
2822
2823 $valid_block_names = array_keys( static::get_blocks_metadata() );
2824 $valid_element_names = array_keys( static::ELEMENTS );
2825 $valid_variations = array();
2826 foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) {
2827 if ( ! isset( $block_meta['styleVariations'] ) ) {
2828 continue;
2829 }
2830 $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
2831 }
2832
2833 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
2834
2835 $blocks_metadata = static::get_blocks_metadata();
2836 $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata );
2837
2838 foreach ( $style_nodes as $metadata ) {
2839 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
2840 if ( empty( $input ) ) {
2841 continue;
2842 }
2843
2844 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
2845 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
2846 $output = $input;
2847 } else {
2848 $output = static::remove_insecure_styles( $input );
2849 }
2850
2851 /*
2852 * Get a reference to element name from path.
2853 * $metadata['path'] = array( 'styles', 'elements', 'link' );
2854 */
2855 $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
2856
2857 /*
2858 * $output is stripped of pseudo selectors. Re-add and process them
2859 * or insecure styles here.
2860 */
2861 // TODO: Replace array_key_exists() with isset() check once WordPress drops
2862 // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2863 if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2864 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
2865 if ( isset( $input[ $pseudo_selector ] ) ) {
2866 $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
2867 }
2868 }
2869 }
2870
2871 if ( ! empty( $output ) ) {
2872 _wp_array_set( $sanitized, $metadata['path'], $output );
2873 }
2874 }
2875
2876 $setting_nodes = static::get_setting_nodes( $theme_json );
2877 foreach ( $setting_nodes as $metadata ) {
2878 $input = _wp_array_get( $theme_json, $metadata['path'], array() );
2879 if ( empty( $input ) ) {
2880 continue;
2881 }
2882
2883 $output = static::remove_insecure_settings( $input );
2884 if ( ! empty( $output ) ) {
2885 _wp_array_set( $sanitized, $metadata['path'], $output );
2886 }
2887 }
2888
2889 if ( empty( $sanitized['styles'] ) ) {
2890 unset( $theme_json['styles'] );
2891 } else {
2892 $theme_json['styles'] = $sanitized['styles'];
2893 }
2894
2895 if ( empty( $sanitized['settings'] ) ) {
2896 unset( $theme_json['settings'] );
2897 } else {
2898 $theme_json['settings'] = $sanitized['settings'];
2899 }
2900
2901 return $theme_json;
2902 }
2903
2904 /**
2905 * Processes a setting node and returns the same node
2906 * without the insecure settings.
2907 *
2908 * @since 5.9.0
2909 *
2910 * @param array $input Node to process.
2911 * @return array
2912 */
2913 protected static function remove_insecure_settings( $input ) {
2914 $output = array();
2915 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2916 foreach ( static::VALID_ORIGINS as $origin ) {
2917 $path_with_origin = $preset_metadata['path'];
2918 $path_with_origin[] = $origin;
2919 $presets = _wp_array_get( $input, $path_with_origin, null );
2920 if ( null === $presets ) {
2921 continue;
2922 }
2923
2924 $escaped_preset = array();
2925 foreach ( $presets as $preset ) {
2926 if (
2927 esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
2928 sanitize_html_class( $preset['slug'] ) === $preset['slug']
2929 ) {
2930 $value = null;
2931 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
2932 $value = $preset[ $preset_metadata['value_key'] ];
2933 } elseif (
2934 isset( $preset_metadata['value_func'] ) &&
2935 is_callable( $preset_metadata['value_func'] )
2936 ) {
2937 $value = call_user_func( $preset_metadata['value_func'], $preset );
2938 }
2939
2940 $preset_is_valid = true;
2941 foreach ( $preset_metadata['properties'] as $property ) {
2942 if ( ! static::is_safe_css_declaration( $property, $value ) ) {
2943 $preset_is_valid = false;
2944 break;
2945 }
2946 }
2947
2948 if ( $preset_is_valid ) {
2949 $escaped_preset[] = $preset;
2950 }
2951 }
2952 }
2953
2954 if ( ! empty( $escaped_preset ) ) {
2955 _wp_array_set( $output, $path_with_origin, $escaped_preset );
2956 }
2957 }
2958 }
2959
2960 // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
2961 static::remove_indirect_properties( $input, $output );
2962
2963 return $output;
2964 }
2965
2966 /**
2967 * Processes a style node and returns the same node
2968 * without the insecure styles.
2969 *
2970 * @since 5.9.0
2971 * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`.
2972 *
2973 * @param array $input Node to process.
2974 * @return array
2975 */
2976 protected static function remove_insecure_styles( $input ) {
2977 $output = array();
2978 $declarations = static::compute_style_properties( $input );
2979
2980 foreach ( $declarations as $declaration ) {
2981 if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
2982 $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
2983
2984 // Check the value isn't an array before adding so as to not
2985 // double up shorthand and longhand styles.
2986 $value = _wp_array_get( $input, $path, array() );
2987 if ( ! is_array( $value ) ) {
2988 _wp_array_set( $output, $path, $value );
2989 }
2990 }
2991 }
2992
2993 // Ensure indirect properties not handled by `compute_style_properties` are allowed.
2994 static::remove_indirect_properties( $input, $output );
2995
2996 return $output;
2997 }
2998
2999 /**
3000 * Checks that a declaration provided by the user is safe.
3001 *
3002 * @since 5.9.0
3003 *
3004 * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`.
3005 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
3006 * @return bool
3007 */
3008 protected static function is_safe_css_declaration( $property_name, $property_value ) {
3009 $style_to_validate = $property_name . ': ' . $property_value;
3010 $filtered = esc_html( safecss_filter_attr( $style_to_validate ) );
3011 return ! empty( trim( $filtered ) );
3012 }
3013
3014 /**
3015 * Removes indirect properties from the given input node and
3016 * sets in the given output node.
3017 *
3018 * @since 6.2.0
3019 *
3020 * @param array $input Node to process.
3021 * @param array $output The processed node. Passed by reference.
3022 */
3023 private static function remove_indirect_properties( $input, &$output ) {
3024 foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
3025 foreach ( $paths as $path ) {
3026 $value = _wp_array_get( $input, $path );
3027 if (
3028 is_string( $value ) &&
3029 static::is_safe_css_declaration( $property, $value )
3030 ) {
3031 _wp_array_set( $output, $path, $value );
3032 }
3033 }
3034 }
3035 }
3036
3037 /**
3038 * Returns the raw data.
3039 *
3040 * @since 5.8.0
3041 *
3042 * @return array Raw data.
3043 */
3044 public function get_raw_data() {
3045 return $this->theme_json;
3046 }
3047
3048 /**
3049 * Transforms the given editor settings according the
3050 * add_theme_support format to the theme.json format.
3051 *
3052 * @since 5.8.0
3053 *
3054 * @param array $settings Existing editor settings.
3055 * @return array Config that adheres to the theme.json schema.
3056 */
3057 public static function get_from_editor_settings( $settings ) {
3058 $theme_settings = array(
3059 'version' => static::LATEST_SCHEMA,
3060 'settings' => array(),
3061 );
3062
3063 // Deprecated theme supports.
3064 if ( isset( $settings['disableCustomColors'] ) ) {
3065 if ( ! isset( $theme_settings['settings']['color'] ) ) {
3066 $theme_settings['settings']['color'] = array();
3067 }
3068 $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
3069 }
3070
3071 if ( isset( $settings['disableCustomGradients'] ) ) {
3072 if ( ! isset( $theme_settings['settings']['color'] ) ) {
3073 $theme_settings['settings']['color'] = array();
3074 }
3075 $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
3076 }
3077
3078 if ( isset( $settings['disableCustomFontSizes'] ) ) {
3079 if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3080 $theme_settings['settings']['typography'] = array();
3081 }
3082 $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
3083 }
3084
3085 if ( isset( $settings['enableCustomLineHeight'] ) ) {
3086 if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3087 $theme_settings['settings']['typography'] = array();
3088 }
3089 $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
3090 }
3091
3092 if ( isset( $settings['enableCustomUnits'] ) ) {
3093 if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
3094 $theme_settings['settings']['spacing'] = array();
3095 }
3096 $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
3097 array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
3098 $settings['enableCustomUnits'];
3099 }
3100
3101 if ( isset( $settings['colors'] ) ) {
3102 if ( ! isset( $theme_settings['settings']['color'] ) ) {
3103 $theme_settings['settings']['color'] = array();
3104 }
3105 $theme_settings['settings']['color']['palette'] = $settings['colors'];
3106 }
3107
3108 if ( isset( $settings['gradients'] ) ) {
3109 if ( ! isset( $theme_settings['settings']['color'] ) ) {
3110 $theme_settings['settings']['color'] = array();
3111 }
3112 $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
3113 }
3114
3115 if ( isset( $settings['fontSizes'] ) ) {
3116 $font_sizes = $settings['fontSizes'];
3117 // Back-compatibility for presets without units.
3118 foreach ( $font_sizes as $key => $font_size ) {
3119 if ( is_numeric( $font_size['size'] ) ) {
3120 $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
3121 }
3122 }
3123 if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3124 $theme_settings['settings']['typography'] = array();
3125 }
3126 $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
3127 }
3128
3129 if ( isset( $settings['enableCustomSpacing'] ) ) {
3130 if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
3131 $theme_settings['settings']['spacing'] = array();
3132 }
3133 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
3134 }
3135
3136 return $theme_settings;
3137 }
3138
3139 /**
3140 * Returns the current theme's wanted patterns(slugs) to be
3141 * registered from Pattern Directory.
3142 *
3143 * @since 6.0.0
3144 *
3145 * @return string[]
3146 */
3147 public function get_patterns() {
3148 if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
3149 return $this->theme_json['patterns'];
3150 }
3151 return array();
3152 }
3153
3154 /**
3155 * Returns a valid theme.json as provided by a theme.
3156 *
3157 * Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
3158 * This also uses appearanceTools instead of their opt-ins if all of them are true.
3159 *
3160 * @since 6.0.0
3161 *
3162 * @return array
3163 */
3164 public function get_data() {
3165 $output = $this->theme_json;
3166 $nodes = static::get_setting_nodes( $output );
3167
3168 /**
3169 * Flatten the theme & custom origins into a single one.
3170 *
3171 * For example, the following:
3172 *
3173 * {
3174 * "settings": {
3175 * "color": {
3176 * "palette": {
3177 * "theme": [ {} ],
3178 * "custom": [ {} ]
3179 * }
3180 * }
3181 * }
3182 * }
3183 *
3184 * will be converted to:
3185 *
3186 * {
3187 * "settings": {
3188 * "color": {
3189 * "palette": [ {} ]
3190 * }
3191 * }
3192 * }
3193 */
3194 foreach ( $nodes as $node ) {
3195 foreach ( static::PRESETS_METADATA as $preset_metadata ) {
3196 $path = $node['path'];
3197 foreach ( $preset_metadata['path'] as $preset_metadata_path ) {
3198 $path[] = $preset_metadata_path;
3199 }
3200 $preset = _wp_array_get( $output, $path, null );
3201 if ( null === $preset ) {
3202 continue;
3203 }
3204
3205 $items = array();
3206 if ( isset( $preset['theme'] ) ) {
3207 foreach ( $preset['theme'] as $item ) {
3208 $slug = $item['slug'];
3209 unset( $item['slug'] );
3210 $items[ $slug ] = $item;
3211 }
3212 }
3213 if ( isset( $preset['custom'] ) ) {
3214 foreach ( $preset['custom'] as $item ) {
3215 $slug = $item['slug'];
3216 unset( $item['slug'] );
3217 $items[ $slug ] = $item;
3218 }
3219 }
3220 $flattened_preset = array();
3221 foreach ( $items as $slug => $value ) {
3222 $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value );
3223 }
3224 _wp_array_set( $output, $path, $flattened_preset );
3225 }
3226 }
3227
3228 // If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
3229 // this code unsets them and sets 'appearanceTools' instead.
3230 foreach ( $nodes as $node ) {
3231 $all_opt_ins_are_set = true;
3232 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
3233 $full_path = $node['path'];
3234 foreach ( $opt_in_path as $opt_in_path_item ) {
3235 $full_path[] = $opt_in_path_item;
3236 }
3237 // Use "unset prop" as a marker instead of "null" because
3238 // "null" can be a valid value for some props (e.g. blockGap).
3239 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
3240 if ( 'unset prop' === $opt_in_value ) {
3241 $all_opt_ins_are_set = false;
3242 break;
3243 }
3244 }
3245
3246 if ( $all_opt_ins_are_set ) {
3247 $node_path_with_appearance_tools = $node['path'];
3248 $node_path_with_appearance_tools[] = 'appearanceTools';
3249 _wp_array_set( $output, $node_path_with_appearance_tools, true );
3250 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
3251 $full_path = $node['path'];
3252 foreach ( $opt_in_path as $opt_in_path_item ) {
3253 $full_path[] = $opt_in_path_item;
3254 }
3255 // Use "unset prop" as a marker instead of "null" because
3256 // "null" can be a valid value for some props (e.g. blockGap).
3257 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
3258 if ( true !== $opt_in_value ) {
3259 continue;
3260 }
3261
3262 // The following could be improved to be path independent.
3263 // At the moment it relies on a couple of assumptions:
3264 //
3265 // - all opt-ins having a path of size 2.
3266 // - there's two sources of settings: the top-level and the block-level.
3267 if (
3268 ( 1 === count( $node['path'] ) ) &&
3269 ( 'settings' === $node['path'][0] )
3270 ) {
3271 // Top-level settings.
3272 unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
3273 if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
3274 unset( $output['settings'][ $opt_in_path[0] ] );
3275 }
3276 } elseif (
3277 ( 3 === count( $node['path'] ) ) &&
3278 ( 'settings' === $node['path'][0] ) &&
3279 ( 'blocks' === $node['path'][1] )
3280 ) {
3281 // Block-level settings.
3282 $block_name = $node['path'][2];
3283 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
3284 if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
3285 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
3286 }
3287 }
3288 }
3289 }
3290 }
3291
3292 wp_recursive_ksort( $output );
3293
3294 return $output;
3295 }
3296
3297 /**
3298 * Sets the spacingSizes array based on the spacingScale values from theme.json.
3299 *
3300 * @since 6.1.0
3301 *
3302 * @return null|void
3303 */
3304 public function set_spacing_sizes() {
3305 $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() );
3306
3307 // Gutenberg didn't have the 1st isset check.
3308 if ( ! isset( $spacing_scale['steps'] )
3309 || ! is_numeric( $spacing_scale['steps'] )
3310 || ! isset( $spacing_scale['mediumStep'] )
3311 || ! isset( $spacing_scale['unit'] )
3312 || ! isset( $spacing_scale['operator'] )
3313 || ! isset( $spacing_scale['increment'] )
3314 || ! isset( $spacing_scale['steps'] )
3315 || ! is_numeric( $spacing_scale['increment'] )
3316 || ! is_numeric( $spacing_scale['mediumStep'] )
3317 || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) {
3318 if ( ! empty( $spacing_scale ) ) {
3319 trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid', 'gutenberg' ), E_USER_NOTICE );
3320 }
3321 return null;
3322 }
3323
3324 // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0.
3325 if ( 0 === $spacing_scale['steps'] ) {
3326 return null;
3327 }
3328
3329 $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
3330 $current_step = $spacing_scale['mediumStep'];
3331 $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
3332 $x_small_count = null;
3333 $below_sizes = array();
3334 $slug = 40;
3335 $remainder = 0;
3336
3337 for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) {
3338 if ( '+' === $spacing_scale['operator'] ) {
3339 $current_step -= $spacing_scale['increment'];
3340 } elseif ( $spacing_scale['increment'] > 1 ) {
3341 $current_step /= $spacing_scale['increment'];
3342 } else {
3343 $current_step *= $spacing_scale['increment'];
3344 }
3345
3346 if ( $current_step <= 0 ) {
3347 $remainder = $below_midpoint_count;
3348 break;
3349 }
3350
3351 $below_sizes[] = array(
3352 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */
3353 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), (string) $x_small_count ),
3354 'slug' => (string) $slug,
3355 'size' => round( $current_step, 2 ) . $unit,
3356 );
3357
3358 if ( $below_midpoint_count === $steps_mid_point - 2 ) {
3359 $x_small_count = 2;
3360 }
3361
3362 if ( $below_midpoint_count < $steps_mid_point - 2 ) {
3363 $x_small_count++;
3364 }
3365
3366 $slug -= 10;
3367 }
3368
3369 $below_sizes = array_reverse( $below_sizes );
3370
3371 $below_sizes[] = array(
3372 'name' => __( 'Medium', 'gutenberg' ),
3373 'slug' => '50',
3374 'size' => $spacing_scale['mediumStep'] . $unit,
3375 );
3376
3377 $current_step = $spacing_scale['mediumStep'];
3378 $x_large_count = null;
3379 $above_sizes = array();
3380 $slug = 60;
3381 $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder;
3382
3383 for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) {
3384 $current_step = '+' === $spacing_scale['operator']
3385 ? $current_step + $spacing_scale['increment']
3386 : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] );
3387
3388 $above_sizes[] = array(
3389 /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */
3390 'name' => 0 === $above_midpoint_count ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), (string) $x_large_count ),
3391 'slug' => (string) $slug,
3392 'size' => round( $current_step, 2 ) . $unit,
3393 );
3394
3395 if ( 1 === $above_midpoint_count ) {
3396 $x_large_count = 2;
3397 }
3398
3399 if ( $above_midpoint_count > 1 ) {
3400 $x_large_count++;
3401 }
3402
3403 $slug += 10;
3404 }
3405
3406 $spacing_sizes = $below_sizes;
3407 foreach ( $above_sizes as $above_sizes_item ) {
3408 $spacing_sizes[] = $above_sizes_item;
3409 }
3410
3411 // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes.
3412 if ( $spacing_scale['steps'] <= 7 ) {
3413 for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
3414 $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
3415 }
3416 }
3417
3418 _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
3419 }
3420
3421 /**
3422 * Returns the selectors metadata for a block.
3423 *
3424 * @param object $block_type The block type.
3425 * @param string $root_selector The block's root selector.
3426 *
3427 * @return object The custom selectors set by the block.
3428 */
3429 protected static function get_block_selectors( $block_type, $root_selector ) {
3430 if ( ! empty( $block_type->selectors ) ) {
3431 return $block_type->selectors;
3432 }
3433
3434 $selectors = array( 'root' => $root_selector );
3435 foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) {
3436 $feature_selector = wp_get_block_css_selector( $block_type, $key );
3437 if ( null !== $feature_selector ) {
3438 $selectors[ $feature ] = array( 'root' => $feature_selector );
3439 }
3440 }
3441
3442 return $selectors;
3443 }
3444
3445 /**
3446 * Generates all the element selectors for a block.
3447 *
3448 * @param string $root_selector The block's root CSS selector.
3449 * @return array The block's element selectors.
3450 */
3451 protected static function get_block_element_selectors( $root_selector ) {
3452 // Assign defaults, then override those that the block sets by itself.
3453 // If the block selector is compounded, will append the element to each
3454 // individual block selector.
3455 $block_selectors = explode( ',', $root_selector );
3456 $element_selectors = array();
3457
3458 foreach ( static::ELEMENTS as $el_name => $el_selector ) {
3459 $element_selector = array();
3460 foreach ( $block_selectors as $selector ) {
3461 if ( $selector === $el_selector ) {
3462 $element_selector = array( $el_selector );
3463 break;
3464 }
3465 $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' );
3466 }
3467 $element_selectors[ $el_name ] = implode( ',', $element_selector );
3468 }
3469
3470 return $element_selectors;
3471 }
3472
3473 /**
3474 * Generates style declarations for a node's features e.g. color, border,
3475 * typography etc, that have custom selectors in their related block's
3476 * metadata.
3477 *
3478 * @param object $metadata The related block metadata containing selectors.
3479 * @param object $node A merged theme.json node for block or variation.
3480 *
3481 * @return array The style declarations for the node's features with custom
3482 * selectors.
3483 */
3484 protected function get_feature_declarations_for_node( $metadata, &$node ) {
3485 $declarations = array();
3486
3487 if ( ! isset( $metadata['selectors'] ) ) {
3488 return $declarations;
3489 }
3490
3491 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
3492
3493 foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
3494 // Skip if this is the block's root selector or the block doesn't
3495 // have any styles for the feature.
3496 if ( 'root' === $feature || empty( $node[ $feature ] ) ) {
3497 continue;
3498 }
3499
3500 if ( is_array( $feature_selectors ) ) {
3501 foreach ( $feature_selectors as $subfeature => $subfeature_selector ) {
3502 if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) {
3503 continue;
3504 }
3505
3506 // Create temporary node containing only the subfeature data
3507 // to leverage existing `compute_style_properties` function.
3508 $subfeature_node = array(
3509 $feature => array(
3510 $subfeature => $node[ $feature ][ $subfeature ],
3511 ),
3512 );
3513
3514 // Generate style declarations.
3515 $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json );
3516
3517 // Merge subfeature declarations into feature declarations.
3518 if ( isset( $declarations[ $subfeature_selector ] ) ) {
3519 foreach ( $new_declarations as $new_declaration ) {
3520 $declarations[ $subfeature_selector ][] = $new_declaration;
3521 }
3522 } else {
3523 $declarations[ $subfeature_selector ] = $new_declarations;
3524 }
3525
3526 // Remove the subfeature from the block's node now its
3527 // styles will be included under its own selector not the
3528 // block's.
3529 unset( $node[ $feature ][ $subfeature ] );
3530 }
3531 }
3532
3533 // Now subfeatures have been processed and removed we can process
3534 // feature root selector or simple string selector.
3535 if (
3536 is_string( $feature_selectors ) ||
3537 ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] )
3538 ) {
3539 $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root'];
3540
3541 // Create temporary node containing only the feature data
3542 // to leverage existing `compute_style_properties` function.
3543 $feature_node = array( $feature => $node[ $feature ] );
3544
3545 // Generate the style declarations.
3546 $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json );
3547
3548 // Merge new declarations with any that already exist for
3549 // the feature selector. This may occur when multiple block
3550 // support features use the same custom selector.
3551 if ( isset( $declarations[ $feature_selector ] ) ) {
3552 foreach ( $new_declarations as $new_declaration ) {
3553 $declarations[ $feature_selector ][] = $new_declaration;
3554 }
3555 } else {
3556 $declarations[ $feature_selector ] = $new_declarations;
3557 }
3558
3559 // Remove the feature from the block's node now its styles
3560 // will be included under its own selector not the block's.
3561 unset( $node[ $feature ] );
3562 }
3563 }
3564
3565 return $declarations;
3566 }
3567
3568 /**
3569 * This is used to convert the internal representation of variables to the CSS representation.
3570 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
3571 *
3572 * @since 6.3.0
3573 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
3574 * @return string The converted variable.
3575 */
3576 private static function convert_custom_properties( $value ) {
3577 $prefix = 'var:';
3578 $prefix_len = strlen( $prefix );
3579 $token_in = '|';
3580 $token_out = '--';
3581 if ( 0 === strpos( $value, $prefix ) ) {
3582 $unwrapped_name = str_replace(
3583 $token_in,
3584 $token_out,
3585 substr( $value, $prefix_len )
3586 );
3587 $value = "var(--wp--$unwrapped_name)";
3588 }
3589
3590 return $value;
3591 }
3592
3593 /**
3594 * Given a tree, converts the internal representation of variables to the CSS representation.
3595 * It is recursive and modifies the input in-place.
3596 *
3597 * @since 6.3.0
3598 * @param array $tree Input to process.
3599 * @return array The modified $tree.
3600 */
3601 private static function resolve_custom_css_format( $tree ) {
3602 $prefix = 'var:';
3603
3604 foreach ( $tree as $key => $data ) {
3605 if ( is_string( $data ) && 0 === strpos( $data, $prefix ) ) {
3606 $tree[ $key ] = self::convert_custom_properties( $data );
3607 } elseif ( is_array( $data ) ) {
3608 $tree[ $key ] = self::resolve_custom_css_format( $data );
3609 }
3610 }
3611
3612 return $tree;
3613 }
3614 }
3615