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


Generated : Thu Nov 12 08:20:02 2026 Cross-referenced by PHPXref