[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri May 1 08:20:13 2026 Cross-referenced by PHPXref