[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Wed Oct 14 08:20:33 2026 Cross-referenced by PHPXref