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


Generated : Wed Oct 28 08:20:35 2026 Cross-referenced by PHPXref