[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-theme-json.php (source)

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


Generated : Fri Jun 26 08:20:11 2026 Cross-referenced by PHPXref